diff options
Diffstat (limited to 'tests/scene')
-rw-r--r-- | tests/scene/test_animation.h | 314 | ||||
-rw-r--r-- | tests/scene/test_arraymesh.h | 321 | ||||
-rw-r--r-- | tests/scene/test_audio_stream_wav.h | 243 | ||||
-rw-r--r-- | tests/scene/test_bit_map.h | 441 | ||||
-rw-r--r-- | tests/scene/test_code_edit.h | 3393 | ||||
-rw-r--r-- | tests/scene/test_curve.h | 254 | ||||
-rw-r--r-- | tests/scene/test_gradient.h | 149 | ||||
-rw-r--r-- | tests/scene/test_path_3d.h | 84 | ||||
-rw-r--r-- | tests/scene/test_path_follow_2d.h | 240 | ||||
-rw-r--r-- | tests/scene/test_path_follow_3d.h | 219 | ||||
-rw-r--r-- | tests/scene/test_sprite_frames.h | 247 | ||||
-rw-r--r-- | tests/scene/test_text_edit.h | 4264 | ||||
-rw-r--r-- | tests/scene/test_theme.h | 271 |
13 files changed, 10440 insertions, 0 deletions
diff --git a/tests/scene/test_animation.h b/tests/scene/test_animation.h new file mode 100644 index 0000000000..9199713fd9 --- /dev/null +++ b/tests/scene/test_animation.h @@ -0,0 +1,314 @@ +/*************************************************************************/ +/* test_animation.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_ANIMATION_H +#define TEST_ANIMATION_H + +#include "scene/resources/animation.h" + +#include "tests/test_macros.h" + +namespace TestAnimation { + +TEST_CASE("[Animation] Empty animation getters") { + const Ref<Animation> animation = memnew(Animation); + + CHECK(Math::is_equal_approx(animation->get_length(), real_t(1.0))); + CHECK(Math::is_equal_approx(animation->get_step(), real_t(0.1))); +} + +TEST_CASE("[Animation] Create value track") { + // This creates an animation that makes the node "Enemy" move to the right by + // 100 pixels in 0.5 seconds. + Ref<Animation> animation = memnew(Animation); + const int track_index = animation->add_track(Animation::TYPE_VALUE); + CHECK(track_index == 0); + animation->track_set_path(track_index, NodePath("Enemy:position:x")); + animation->track_insert_key(track_index, 0.0, 0); + animation->track_insert_key(track_index, 0.5, 100); + + CHECK(animation->get_track_count() == 1); + CHECK(!animation->track_is_compressed(0)); + CHECK(int(animation->track_get_key_value(0, 0)) == 0); + CHECK(int(animation->track_get_key_value(0, 1)) == 100); + + CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, -0.2), 0.0)); + CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.0), 0.0)); + CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.2), 40.0)); + CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.4), 80.0)); + CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.5), 100.0)); + CHECK(Math::is_equal_approx(animation->value_track_interpolate(0, 0.6), 100.0)); + + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0))); + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0))); + + ERR_PRINT_OFF; + // Nonexistent keys. + CHECK(animation->track_get_key_value(0, 2).is_null()); + CHECK(animation->track_get_key_value(0, -1).is_null()); + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 2), real_t(-1.0))); + // Nonexistent track (and keys). + CHECK(animation->track_get_key_value(1, 0).is_null()); + CHECK(animation->track_get_key_value(1, 1).is_null()); + CHECK(animation->track_get_key_value(1, 2).is_null()); + CHECK(animation->track_get_key_value(1, -1).is_null()); + CHECK(Math::is_equal_approx(animation->track_get_key_transition(1, 0), real_t(-1.0))); + + // This is a value track, so the methods below should return errors. + CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0))); + CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + ERR_PRINT_ON; +} + +TEST_CASE("[Animation] Create 3D position track") { + Ref<Animation> animation = memnew(Animation); + const int track_index = animation->add_track(Animation::TYPE_POSITION_3D); + animation->track_set_path(track_index, NodePath("Enemy:position")); + animation->position_track_insert_key(track_index, 0.0, Vector3(0, 1, 2)); + animation->position_track_insert_key(track_index, 0.5, Vector3(3.5, 4, 5)); + + CHECK(animation->get_track_count() == 1); + CHECK(!animation->track_is_compressed(0)); + CHECK(Vector3(animation->track_get_key_value(0, 0)).is_equal_approx(Vector3(0, 1, 2))); + CHECK(Vector3(animation->track_get_key_value(0, 1)).is_equal_approx(Vector3(3.5, 4, 5))); + + Vector3 r_interpolation; + + CHECK(animation->position_track_interpolate(0, -0.2, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2))); + + CHECK(animation->position_track_interpolate(0, 0.0, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2))); + + CHECK(animation->position_track_interpolate(0, 0.2, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(1.4, 2.2, 3.2))); + + CHECK(animation->position_track_interpolate(0, 0.4, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(2.8, 3.4, 4.4))); + + CHECK(animation->position_track_interpolate(0, 0.5, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5))); + + CHECK(animation->position_track_interpolate(0, 0.6, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5))); + + // 3D position tracks always use linear interpolation for performance reasons. + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0))); + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0))); + + // This is a 3D position track, so the methods below should return errors. + ERR_PRINT_OFF; + CHECK(animation->value_track_interpolate(0, 0.0).is_null()); + CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0))); + CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + ERR_PRINT_ON; +} + +TEST_CASE("[Animation] Create 3D rotation track") { + Ref<Animation> animation = memnew(Animation); + const int track_index = animation->add_track(Animation::TYPE_ROTATION_3D); + animation->track_set_path(track_index, NodePath("Enemy:rotation")); + animation->rotation_track_insert_key(track_index, 0.0, Quaternion(Vector3(0, 1, 2))); + animation->rotation_track_insert_key(track_index, 0.5, Quaternion(Vector3(3.5, 4, 5))); + + CHECK(animation->get_track_count() == 1); + CHECK(!animation->track_is_compressed(0)); + CHECK(Quaternion(animation->track_get_key_value(0, 0)).is_equal_approx(Quaternion(Vector3(0, 1, 2)))); + CHECK(Quaternion(animation->track_get_key_value(0, 1)).is_equal_approx(Quaternion(Vector3(3.5, 4, 5)))); + + Quaternion r_interpolation; + + CHECK(animation->rotation_track_interpolate(0, -0.2, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Quaternion(0.403423, 0.259035, 0.73846, 0.47416))); + + CHECK(animation->rotation_track_interpolate(0, 0.0, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Quaternion(0.403423, 0.259035, 0.73846, 0.47416))); + + CHECK(animation->rotation_track_interpolate(0, 0.2, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Quaternion(0.336182, 0.30704, 0.751515, 0.477425))); + + CHECK(animation->rotation_track_interpolate(0, 0.4, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Quaternion(0.266585, 0.352893, 0.759303, 0.477344))); + + CHECK(animation->rotation_track_interpolate(0, 0.5, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Quaternion(0.231055, 0.374912, 0.761204, 0.476048))); + + CHECK(animation->rotation_track_interpolate(0, 0.6, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Quaternion(0.231055, 0.374912, 0.761204, 0.476048))); + + // 3D rotation tracks always use linear interpolation for performance reasons. + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0))); + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0))); + + // This is a 3D rotation track, so the methods below should return errors. + ERR_PRINT_OFF; + CHECK(animation->value_track_interpolate(0, 0.0).is_null()); + CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0))); + CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + ERR_PRINT_ON; +} + +TEST_CASE("[Animation] Create 3D scale track") { + Ref<Animation> animation = memnew(Animation); + const int track_index = animation->add_track(Animation::TYPE_SCALE_3D); + animation->track_set_path(track_index, NodePath("Enemy:scale")); + animation->scale_track_insert_key(track_index, 0.0, Vector3(0, 1, 2)); + animation->scale_track_insert_key(track_index, 0.5, Vector3(3.5, 4, 5)); + + CHECK(animation->get_track_count() == 1); + CHECK(!animation->track_is_compressed(0)); + CHECK(Vector3(animation->track_get_key_value(0, 0)).is_equal_approx(Vector3(0, 1, 2))); + CHECK(Vector3(animation->track_get_key_value(0, 1)).is_equal_approx(Vector3(3.5, 4, 5))); + + Vector3 r_interpolation; + + CHECK(animation->scale_track_interpolate(0, -0.2, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2))); + + CHECK(animation->scale_track_interpolate(0, 0.0, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(0, 1, 2))); + + CHECK(animation->scale_track_interpolate(0, 0.2, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(1.4, 2.2, 3.2))); + + CHECK(animation->scale_track_interpolate(0, 0.4, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(2.8, 3.4, 4.4))); + + CHECK(animation->scale_track_interpolate(0, 0.5, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5))); + + CHECK(animation->scale_track_interpolate(0, 0.6, &r_interpolation) == OK); + CHECK(r_interpolation.is_equal_approx(Vector3(3.5, 4, 5))); + + // 3D scale tracks always use linear interpolation for performance reasons. + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0))); + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0))); + + // This is a 3D scale track, so the methods below should return errors. + ERR_PRINT_OFF; + CHECK(animation->value_track_interpolate(0, 0.0).is_null()); + CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0))); + CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + ERR_PRINT_ON; +} + +TEST_CASE("[Animation] Create blend shape track") { + Ref<Animation> animation = memnew(Animation); + const int track_index = animation->add_track(Animation::TYPE_BLEND_SHAPE); + animation->track_set_path(track_index, NodePath("Enemy:scale")); + // Negative values for blend shapes should work as expected. + animation->blend_shape_track_insert_key(track_index, 0.0, -1.0); + animation->blend_shape_track_insert_key(track_index, 0.5, 1.0); + + CHECK(animation->get_track_count() == 1); + CHECK(!animation->track_is_compressed(0)); + + float r_blend = 0.0f; + + CHECK(animation->blend_shape_track_get_key(0, 0, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, -1.0f)); + + CHECK(animation->blend_shape_track_get_key(0, 1, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, 1.0f)); + + CHECK(animation->blend_shape_track_interpolate(0, -0.2, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, -1.0f)); + + CHECK(animation->blend_shape_track_interpolate(0, 0.0, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, -1.0f)); + + CHECK(animation->blend_shape_track_interpolate(0, 0.2, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, -0.2f)); + + CHECK(animation->blend_shape_track_interpolate(0, 0.4, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, 0.6f)); + + CHECK(animation->blend_shape_track_interpolate(0, 0.5, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, 1.0f)); + + CHECK(animation->blend_shape_track_interpolate(0, 0.6, &r_blend) == OK); + CHECK(Math::is_equal_approx(r_blend, 1.0f)); + + // Blend shape tracks always use linear interpolation for performance reasons. + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 0), real_t(1.0))); + CHECK(Math::is_equal_approx(animation->track_get_key_transition(0, 1), real_t(1.0))); + + // This is a blend shape track, so the methods below should return errors. + ERR_PRINT_OFF; + CHECK(animation->value_track_interpolate(0, 0.0).is_null()); + CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(Math::is_zero_approx(animation->bezier_track_interpolate(0, 0.0))); + ERR_PRINT_ON; +} + +TEST_CASE("[Animation] Create Bezier track") { + Ref<Animation> animation = memnew(Animation); + const int track_index = animation->add_track(Animation::TYPE_BEZIER); + animation->track_set_path(track_index, NodePath("Enemy:scale")); + animation->bezier_track_insert_key(track_index, 0.0, -1.0, Vector2(-1, -1), Vector2(1, 1)); + animation->bezier_track_insert_key(track_index, 0.5, 1.0, Vector2(0, 1), Vector2(1, 0.5)); + + CHECK(animation->get_track_count() == 1); + CHECK(!animation->track_is_compressed(0)); + + CHECK(Math::is_equal_approx(animation->bezier_track_get_key_value(0, 0), real_t(-1.0))); + CHECK(Math::is_equal_approx(animation->bezier_track_get_key_value(0, 1), real_t(1.0))); + + CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, -0.2), real_t(-1.0))); + CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.0), real_t(-1.0))); + CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.2), real_t(-0.76057207584381))); + CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.4), real_t(-0.39975279569626))); + CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.5), real_t(1.0))); + CHECK(Math::is_equal_approx(animation->bezier_track_interpolate(0, 0.6), real_t(1.0))); + + // This is a bezier track, so the methods below should return errors. + ERR_PRINT_OFF; + CHECK(animation->value_track_interpolate(0, 0.0).is_null()); + CHECK(animation->position_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->rotation_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->scale_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + CHECK(animation->blend_shape_track_interpolate(0, 0.0, nullptr) == ERR_INVALID_PARAMETER); + ERR_PRINT_ON; +} + +} // namespace TestAnimation + +#endif // TEST_ANIMATION_H diff --git a/tests/scene/test_arraymesh.h b/tests/scene/test_arraymesh.h new file mode 100644 index 0000000000..fc23adcd06 --- /dev/null +++ b/tests/scene/test_arraymesh.h @@ -0,0 +1,321 @@ +/*************************************************************************/ +/* test_arraymesh.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_ARRAYMESH_H +#define TEST_ARRAYMESH_H + +#include "scene/resources/mesh.h" +#include "scene/resources/primitive_meshes.h" + +#include "tests/test_macros.h" + +namespace TestArrayMesh { + +TEST_CASE("[SceneTree][ArrayMesh] Adding and modifying blendshapes.") { + Ref<ArrayMesh> mesh = memnew(ArrayMesh); + StringName name_a{ "ShapeA" }; + StringName name_b{ "ShapeB" }; + + SUBCASE("Adding a blend shape to the mesh before a surface is added.") { + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_b); + + CHECK(mesh->get_blend_shape_name(0) == name_a); + CHECK(mesh->get_blend_shape_name(1) == name_b); + } + + SUBCASE("Add same blend shape multiple times appends name with number.") { + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_a); + + CHECK(mesh->get_blend_shape_name(0) == "ShapeA"); + bool all_different = (static_cast<String>(mesh->get_blend_shape_name(0)) != static_cast<String>(mesh->get_blend_shape_name(1))) && + (static_cast<String>(mesh->get_blend_shape_name(1)) != static_cast<String>(mesh->get_blend_shape_name(2))) && + (static_cast<String>(mesh->get_blend_shape_name(0)) != static_cast<String>(mesh->get_blend_shape_name(2))); + bool all_have_name = static_cast<String>(mesh->get_blend_shape_name(1)).contains("ShapeA") && + static_cast<String>(mesh->get_blend_shape_name(2)).contains("ShapeA"); + CHECK((all_different && all_have_name)); + } + + SUBCASE("ArrayMesh keeps correct count of number of blend shapes") { + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_b); + mesh->add_blend_shape(name_b); + mesh->add_blend_shape(name_b); + + REQUIRE(mesh->get_blend_shape_count() == 5); + } + + SUBCASE("Adding blend shape after surface is added causes error") { + Ref<CylinderMesh> cylinder = memnew(CylinderMesh); + Array cylinder_array{}; + cylinder_array.resize(Mesh::ARRAY_MAX); + cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array); + + mesh->add_blend_shape(name_a); + CHECK(mesh->get_blend_shape_count() == 0); + } + + SUBCASE("Change blend shape name after adding.") { + mesh->add_blend_shape(name_a); + mesh->set_blend_shape_name(0, name_b); + + CHECK(mesh->get_blend_shape_name(0) == name_b); + } + + SUBCASE("Change blend shape name to the name of one already there, should append number to end") { + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_b); + mesh->set_blend_shape_name(0, name_b); + + String name_string = mesh->get_blend_shape_name(0); + CHECK(name_string.contains("ShapeB")); + CHECK(name_string.length() > static_cast<String>(name_b).size()); + } + + SUBCASE("Clear all blend shapes before surface has been added.") { + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_b); + CHECK(mesh->get_blend_shape_count() == 2); + + mesh->clear_blend_shapes(); + CHECK(mesh->get_blend_shape_count() == 0); + } + + SUBCASE("Can't clear blend shapes after surface had been added.") { + mesh->add_blend_shape(name_a); + mesh->add_blend_shape(name_b); + Ref<CylinderMesh> cylinder = memnew(CylinderMesh); + Array cylinder_array{}; + cylinder_array.resize(Mesh::ARRAY_MAX); + cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array); + + mesh->clear_blend_shapes(); + CHECK(mesh->get_blend_shape_count() == 2); + } + + SUBCASE("Set the blend shape mode of ArrayMesh and underlying mesh RID.") { + mesh->set_blend_shape_mode(Mesh::BLEND_SHAPE_MODE_RELATIVE); + CHECK(mesh->get_blend_shape_mode() == Mesh::BLEND_SHAPE_MODE_RELATIVE); + } +} + +TEST_CASE("[SceneTree][ArrayMesh] Surface meta data tests.") { + Ref<ArrayMesh> mesh = memnew(ArrayMesh); + Ref<CylinderMesh> cylinder = memnew(CylinderMesh); + Array cylinder_array{}; + cylinder_array.resize(Mesh::ARRAY_MAX); + cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array); + + Ref<BoxMesh> box = memnew(BoxMesh); + Array box_array{}; + box_array.resize(Mesh::ARRAY_MAX); + box->create_mesh_array(box_array, Vector3(2.f, 1.2f, 1.6f)); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, box_array); + + SUBCASE("Add 2 surfaces and count the number of surfaces in the mesh.") { + REQUIRE(mesh->get_surface_count() == 2); + } + + SUBCASE("Get the surface array from mesh.") { + REQUIRE(mesh->surface_get_arrays(0)[0] == cylinder_array[0]); + REQUIRE(mesh->surface_get_arrays(1)[0] == box_array[0]); + } + + SUBCASE("Get the array length of a particular surface.") { + CHECK(mesh->surface_get_array_len(0) == static_cast<Vector<Vector3>>(cylinder_array[RenderingServer::ARRAY_VERTEX]).size()); + CHECK(mesh->surface_get_array_len(1) == static_cast<Vector<Vector3>>(box_array[RenderingServer::ARRAY_VERTEX]).size()); + } + + SUBCASE("Get the index array length of a particular surface.") { + CHECK(mesh->surface_get_array_index_len(0) == static_cast<Vector<Vector3>>(cylinder_array[RenderingServer::ARRAY_INDEX]).size()); + CHECK(mesh->surface_get_array_index_len(1) == static_cast<Vector<Vector3>>(box_array[RenderingServer::ARRAY_INDEX]).size()); + } + + SUBCASE("Get correct primitive type") { + CHECK(mesh->surface_get_primitive_type(0) == Mesh::PRIMITIVE_TRIANGLES); + CHECK(mesh->surface_get_primitive_type(1) == Mesh::PRIMITIVE_TRIANGLES); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLE_STRIP, box_array); + CHECK(mesh->surface_get_primitive_type(2) == Mesh::PRIMITIVE_TRIANGLE_STRIP); + } + + SUBCASE("Returns correct format for the mesh") { + auto format = RS::ARRAY_FORMAT_BLEND_SHAPE_MASK | RS::ARRAY_FORMAT_TEX_UV | RS::ARRAY_FORMAT_INDEX; + CHECK((mesh->surface_get_format(0) & format) != 0); + CHECK((mesh->surface_get_format(1) & format) != 0); + } + + SUBCASE("Set a surface name and retrieve it by name.") { + mesh->surface_set_name(0, "surf1"); + CHECK(mesh->surface_find_by_name("surf1") == 0); + CHECK(mesh->surface_get_name(0) == "surf1"); + } + + SUBCASE("Set material to two different surfaces.") { + Ref<Material> mat = memnew(Material); + mesh->surface_set_material(0, mat); + CHECK(mesh->surface_get_material(0) == mat); + mesh->surface_set_material(1, mat); + CHECK(mesh->surface_get_material(1) == mat); + } + + SUBCASE("Set same material multiple times doesn't change material of surface.") { + Ref<Material> mat = memnew(Material); + mesh->surface_set_material(0, mat); + mesh->surface_set_material(0, mat); + mesh->surface_set_material(0, mat); + CHECK(mesh->surface_get_material(0) == mat); + } + + SUBCASE("Set material of surface then change to different material.") { + Ref<Material> mat1 = memnew(Material); + Ref<Material> mat2 = memnew(Material); + mesh->surface_set_material(1, mat1); + CHECK(mesh->surface_get_material(1) == mat1); + mesh->surface_set_material(1, mat2); + CHECK(mesh->surface_get_material(1) == mat2); + } + + SUBCASE("Get the LOD of the mesh.") { + Dictionary lod{}; + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array, TypedArray<Array>{}, lod); + CHECK(mesh->surface_get_lods(2) == lod); + } + + SUBCASE("Get the blend shape arrays from the mesh.") { + TypedArray<Array> blend{}; + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array, blend); + CHECK(mesh->surface_get_blend_shape_arrays(2) == blend); + } +} + +TEST_CASE("[SceneTree][ArrayMesh] Get/Set mesh metadata and actions") { + Ref<ArrayMesh> mesh = memnew(ArrayMesh); + Ref<CylinderMesh> cylinder = memnew(CylinderMesh); + Array cylinder_array{}; + cylinder_array.resize(Mesh::ARRAY_MAX); + cylinder->create_mesh_array(cylinder_array, 3.f, 3.f, 5.f); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, cylinder_array); + + Ref<BoxMesh> box = memnew(BoxMesh); + Array box_array{}; + box_array.resize(Mesh::ARRAY_MAX); + box->create_mesh_array(box_array, Vector3(2.f, 1.2f, 1.6f)); + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, box_array); + + SUBCASE("Set the shadow mesh.") { + Ref<ArrayMesh> shadow = memnew(ArrayMesh); + mesh->set_shadow_mesh(shadow); + CHECK(mesh->get_shadow_mesh() == shadow); + } + + SUBCASE("Set the shadow mesh multiple times.") { + Ref<ArrayMesh> shadow = memnew(ArrayMesh); + mesh->set_shadow_mesh(shadow); + mesh->set_shadow_mesh(shadow); + mesh->set_shadow_mesh(shadow); + mesh->set_shadow_mesh(shadow); + CHECK(mesh->get_shadow_mesh() == shadow); + } + + SUBCASE("Set the same shadow mesh on multiple meshes.") { + Ref<ArrayMesh> shadow = memnew(ArrayMesh); + Ref<ArrayMesh> mesh2 = memnew(ArrayMesh); + mesh->set_shadow_mesh(shadow); + mesh2->set_shadow_mesh(shadow); + + CHECK(mesh->get_shadow_mesh() == shadow); + CHECK(mesh2->get_shadow_mesh() == shadow); + } + + SUBCASE("Set the shadow mesh and then change it.") { + Ref<ArrayMesh> shadow = memnew(ArrayMesh); + mesh->set_shadow_mesh(shadow); + CHECK(mesh->get_shadow_mesh() == shadow); + Ref<ArrayMesh> shadow2 = memnew(ArrayMesh); + mesh->set_shadow_mesh(shadow2); + CHECK(mesh->get_shadow_mesh() == shadow2); + } + + SUBCASE("Set custom AABB.") { + AABB bound{}; + mesh->set_custom_aabb(bound); + CHECK(mesh->get_custom_aabb() == bound); + } + + SUBCASE("Set custom AABB multiple times.") { + AABB bound{}; + mesh->set_custom_aabb(bound); + mesh->set_custom_aabb(bound); + mesh->set_custom_aabb(bound); + mesh->set_custom_aabb(bound); + CHECK(mesh->get_custom_aabb() == bound); + } + + SUBCASE("Set custom AABB then change to another AABB.") { + AABB bound{}; + AABB bound2{}; + mesh->set_custom_aabb(bound); + CHECK(mesh->get_custom_aabb() == bound); + mesh->set_custom_aabb(bound2); + CHECK(mesh->get_custom_aabb() == bound2); + } + + SUBCASE("Clear all surfaces should leave zero count.") { + mesh->clear_surfaces(); + CHECK(mesh->get_surface_count() == 0); + } + + SUBCASE("Able to get correct mesh RID.") { + RID rid = mesh->get_rid(); + CHECK(RS::get_singleton()->mesh_get_surface_count(rid) == 2); + } + + SUBCASE("Create surface from raw SurfaceData data.") { + RID mesh_rid = mesh->get_rid(); + RS::SurfaceData surface_data = RS::get_singleton()->mesh_get_surface(mesh_rid, 0); + Ref<ArrayMesh> mesh2 = memnew(ArrayMesh); + mesh2->add_surface(surface_data.format, Mesh::PRIMITIVE_TRIANGLES, surface_data.vertex_data, surface_data.attribute_data, + surface_data.skin_data, surface_data.vertex_count, surface_data.index_data, surface_data.index_count, surface_data.aabb); + CHECK(mesh2->get_surface_count() == 1); + CHECK(mesh2->surface_get_primitive_type(0) == Mesh::PRIMITIVE_TRIANGLES); + CHECK((mesh2->surface_get_format(0) & surface_data.format) != 0); + CHECK(mesh2->get_aabb().is_equal_approx(surface_data.aabb)); + } +} + +} // namespace TestArrayMesh + +#endif // TEST_ARRAYMESH_H diff --git a/tests/scene/test_audio_stream_wav.h b/tests/scene/test_audio_stream_wav.h new file mode 100644 index 0000000000..4ba431dfc2 --- /dev/null +++ b/tests/scene/test_audio_stream_wav.h @@ -0,0 +1,243 @@ +/*************************************************************************/ +/* test_audio_stream_wav.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_AUDIO_STREAM_WAV_H +#define TEST_AUDIO_STREAM_WAV_H + +#include "core/math/math_defs.h" +#include "core/math/math_funcs.h" +#include "scene/resources/audio_stream_wav.h" + +#include "tests/test_macros.h" + +#ifdef TOOLS_ENABLED +#include "core/io/resource_loader.h" +#include "editor/import/resource_importer_wav.h" +#endif + +namespace TestAudioStreamWAV { + +// Default wav rate for test cases. +constexpr float WAV_RATE = 44100; +/* Default wav count for test cases. 1 second of audio is used so that the file can be listened +to manually if needed. */ +constexpr int WAV_COUNT = WAV_RATE; + +float gen_wav(float frequency, float wav_rate, int wav_number) { + // formula for generating a sin wave with given frequency. + return Math::sin((Math_TAU * frequency / wav_rate) * wav_number); +} + +/* Generates a 440Hz sin wave in channel 0 (mono channel or left stereo channel) + * and a 261.63Hz wave in channel 1 (right stereo channel). + * These waves correspond to the music notes A4 and C4 respectively. + */ +Vector<uint8_t> gen_pcm8_test(float wav_rate, int wav_count, bool stereo) { + Vector<uint8_t> buffer; + buffer.resize(stereo ? wav_count * 2 : wav_count); + + uint8_t *write_ptr = buffer.ptrw(); + for (int i = 0; i < buffer.size(); i++) { + float wav; + if (stereo) { + if (i % 2 == 0) { + wav = gen_wav(440, wav_rate, i / 2); + } else { + wav = gen_wav(261.63, wav_rate, i / 2); + } + } else { + wav = gen_wav(440, wav_rate, i); + } + + // Map sin wave to full range of 8-bit values. + uint8_t wav_8bit = Math::fast_ftoi(((wav + 1) / 2) * UINT8_MAX); + // Unlike the .wav format, AudioStreamWAV expects signed 8-bit wavs. + uint8_t wav_8bit_signed = wav_8bit - (INT8_MAX + 1); + write_ptr[i] = wav_8bit_signed; + } + + return buffer; +} + +// Same as gen_pcm8_test but with 16-bit wavs. +Vector<uint8_t> gen_pcm16_test(float wav_rate, int wav_count, bool stereo) { + Vector<uint8_t> buffer; + buffer.resize(stereo ? wav_count * 4 : wav_count * 2); + + uint8_t *write_ptr = buffer.ptrw(); + for (int i = 0; i < buffer.size() / 2; i++) { + float wav; + if (stereo) { + if (i % 2 == 0) { + wav = gen_wav(440, wav_rate, i / 2); + } else { + wav = gen_wav(261.63, wav_rate, i / 2); + } + } else { + wav = gen_wav(440, wav_rate, i); + } + + // Map sin wave to full range of 16-bit values. + uint16_t wav_16bit = Math::fast_ftoi(((wav + 1) / 2) * UINT16_MAX); + // The .wav format expects wavs larger than 8 bits to be signed. + uint16_t wav_16bit_signed = wav_16bit - (INT16_MAX + 1); + encode_uint16(wav_16bit_signed, write_ptr + (i * 2)); + } + + return buffer; +} + +void run_test(String file_name, AudioStreamWAV::Format data_format, bool stereo, float wav_rate, float wav_count) { + String save_path = OS::get_singleton()->get_cache_path().path_join(file_name); + + Vector<uint8_t> test_data; + if (data_format == AudioStreamWAV::FORMAT_8_BITS) { + test_data = gen_pcm8_test(wav_rate, wav_count, stereo); + } else { + test_data = gen_pcm16_test(wav_rate, wav_count, stereo); + } + + Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV); + stream->set_mix_rate(wav_rate); + CHECK(stream->get_mix_rate() == wav_rate); + + stream->set_format(data_format); + CHECK(stream->get_format() == data_format); + + stream->set_stereo(stereo); + CHECK(stream->is_stereo() == stereo); + + stream->set_data(test_data); + CHECK(stream->get_data() == test_data); + + SUBCASE("Stream length is computed properly") { + CHECK(Math::is_equal_approx(stream->get_length(), double(wav_count / wav_rate))); + } + + SUBCASE("Stream can be saved as .wav") { + REQUIRE(stream->save_to_wav(save_path) == OK); + + Error error; + Ref<FileAccess> wav_file = FileAccess::open(save_path, FileAccess::READ, &error); + REQUIRE(error == OK); + +#if TOOLS_ENABLED + // The WAV importer can be used if enabled to check that the saved file is valid. + Ref<ResourceImporterWAV> wav_importer = memnew(ResourceImporterWAV); + + List<ResourceImporter::ImportOption> options_list; + wav_importer->get_import_options("", &options_list); + + HashMap<StringName, Variant> options_map; + for (const ResourceImporter::ImportOption &E : options_list) { + options_map[E.option.name] = E.default_value; + } + + REQUIRE(wav_importer->import(save_path, save_path, options_map, nullptr) == OK); + + String load_path = save_path + "." + wav_importer->get_save_extension(); + Ref<AudioStreamWAV> loaded_stream = ResourceLoader::load(load_path, "AudioStreamWAV", ResourceFormatImporter::CACHE_MODE_IGNORE, &error); + REQUIRE(error == OK); + + CHECK(loaded_stream->get_format() == stream->get_format()); + CHECK(loaded_stream->get_loop_mode() == stream->get_loop_mode()); + CHECK(loaded_stream->get_loop_begin() == stream->get_loop_begin()); + CHECK(loaded_stream->get_loop_end() == stream->get_loop_end()); + CHECK(loaded_stream->get_mix_rate() == stream->get_mix_rate()); + CHECK(loaded_stream->is_stereo() == stream->is_stereo()); + CHECK(loaded_stream->get_length() == stream->get_length()); + CHECK(loaded_stream->is_monophonic() == stream->is_monophonic()); + CHECK(loaded_stream->get_data() == stream->get_data()); +#endif + } +} + +TEST_CASE("[AudioStreamWAV] Mono PCM8 format") { + run_test("test_pcm8_mono.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, WAV_COUNT); +} + +TEST_CASE("[AudioStreamWAV] Mono PCM16 format") { + run_test("test_pcm16_mono.wav", AudioStreamWAV::FORMAT_16_BITS, false, WAV_RATE, WAV_COUNT); +} + +TEST_CASE("[AudioStreamWAV] Stereo PCM8 format") { + run_test("test_pcm8_stereo.wav", AudioStreamWAV::FORMAT_8_BITS, true, WAV_RATE, WAV_COUNT); +} + +TEST_CASE("[AudioStreamWAV] Stereo PCM16 format") { + run_test("test_pcm16_stereo.wav", AudioStreamWAV::FORMAT_16_BITS, true, WAV_RATE, WAV_COUNT); +} + +TEST_CASE("[AudioStreamWAV] Alternate mix rate") { + run_test("test_pcm16_stereo_38000Hz.wav", AudioStreamWAV::FORMAT_16_BITS, true, 38000, 38000); +} + +TEST_CASE("[AudioStreamWAV] save_to_wav() adds '.wav' file extension automatically") { + String save_path = OS::get_singleton()->get_cache_path().path_join("test_wav_extension"); + Vector<uint8_t> test_data = gen_pcm8_test(WAV_RATE, WAV_COUNT, false); + Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV); + stream->set_data(test_data); + + REQUIRE(stream->save_to_wav(save_path) == OK); + Error error; + Ref<FileAccess> wav_file = FileAccess::open(save_path + ".wav", FileAccess::READ, &error); + CHECK(error == OK); +} + +TEST_CASE("[AudioStreamWAV] Default values") { + Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV); + CHECK(stream->get_format() == AudioStreamWAV::FORMAT_8_BITS); + CHECK(stream->get_loop_mode() == AudioStreamWAV::LOOP_DISABLED); + CHECK(stream->get_loop_begin() == 0); + CHECK(stream->get_loop_end() == 0); + CHECK(stream->get_mix_rate() == 44100); + CHECK(stream->is_stereo() == false); + CHECK(stream->get_length() == 0); + CHECK(stream->is_monophonic() == false); + CHECK(stream->get_data() == Vector<uint8_t>{}); + CHECK(stream->get_stream_name() == ""); +} + +TEST_CASE("[AudioStreamWAV] Save empty file") { + run_test("test_empty.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, 0); +} + +TEST_CASE("[AudioStreamWAV] Saving IMA ADPCM is not supported") { + String save_path = OS::get_singleton()->get_cache_path().path_join("test_adpcm.wav"); + Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV); + stream->set_format(AudioStreamWAV::FORMAT_IMA_ADPCM); + ERR_PRINT_OFF; + CHECK(stream->save_to_wav(save_path) == ERR_UNAVAILABLE); + ERR_PRINT_ON; +} + +} // namespace TestAudioStreamWAV + +#endif // TEST_AUDIO_STREAM_WAV_H diff --git a/tests/scene/test_bit_map.h b/tests/scene/test_bit_map.h new file mode 100644 index 0000000000..a102f40725 --- /dev/null +++ b/tests/scene/test_bit_map.h @@ -0,0 +1,441 @@ +/*************************************************************************/ +/* test_bit_map.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_BIT_MAP_H +#define TEST_BIT_MAP_H + +#include "core/os/memory.h" +#include "scene/resources/bit_map.h" +#include "tests/test_macros.h" + +namespace TestBitmap { + +void reset_bit_map(BitMap &p_bm) { + Size2i size = p_bm.get_size(); + p_bm.set_bit_rect(Rect2i(0, 0, size.width, size.height), false); +} + +TEST_CASE("[BitMap] Create bit map") { + Size2i dim{ 256, 512 }; + BitMap bit_map{}; + bit_map.create(dim); + CHECK(bit_map.get_size() == Size2i(256, 512)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "This will go through the entire bitmask inside of bitmap, thus hopefully checking if the bitmask was correctly set up."); + + dim = Size2i(0, 256); + bit_map.create(dim); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 512), "We should still have the same dimensions as before, because the new dimension is invalid."); + + dim = Size2i(512, 0); + bit_map.create(dim); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 512), "We should still have the same dimensions as before, because the new dimension is invalid."); + + dim = Size2i(46341, 46341); + bit_map.create(dim); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 512), "We should still have the same dimensions as before, because the new dimension is too large (46341*46341=2147488281)."); +} + +TEST_CASE("[BitMap] Create bit map from image alpha") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + bit_map.create(dim); + + const Ref<Image> null_img = nullptr; + bit_map.create_from_image_alpha(null_img); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Bitmap should have its old values because bitmap creation from a nullptr should fail."); + + Ref<Image> empty_img; + empty_img.instantiate(); + bit_map.create_from_image_alpha(empty_img); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Bitmap should have its old values because bitmap creation from an empty image should fail."); + + Ref<Image> wrong_format_img = Image::create_empty(3, 3, false, Image::Format::FORMAT_DXT1); + bit_map.create_from_image_alpha(wrong_format_img); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Bitmap should have its old values because converting from a compressed image should fail."); + + Ref<Image> img = Image::create_empty(3, 3, false, Image::Format::FORMAT_RGBA8); + img->set_pixel(0, 0, Color(0, 0, 0, 0)); + img->set_pixel(0, 1, Color(0, 0, 0, 0.09f)); + img->set_pixel(0, 2, Color(0, 0, 0, 0.25f)); + img->set_pixel(1, 0, Color(0, 0, 0, 0.5f)); + img->set_pixel(1, 1, Color(0, 0, 0, 0.75f)); + img->set_pixel(1, 2, Color(0, 0, 0, 0.99f)); + img->set_pixel(2, 0, Color(0, 0, 0, 1.f)); + + // Check different threshold values. + bit_map.create_from_image_alpha(img); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 5, "There are 5 values in the image that are smaller than the default threshold of 0.1."); + + bit_map.create_from_image_alpha(img, 0.08f); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 6, "There are 6 values in the image that are smaller than the threshold of 0.08."); + + bit_map.create_from_image_alpha(img, 1); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "There are no values in the image that are smaller than the threshold of 1, there is one value equal to 1, but we check for inequality only."); +} + +TEST_CASE("[BitMap] Set bit") { + Size2i dim{ 256, 256 }; + BitMap bit_map{}; + + // Setting a point before a bit map is created should not crash, because there are checks to see if we are out of bounds. + bit_map.set_bitv(Point2i(128, 128), true); + + bit_map.create(dim); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "All values should be initialized to false."); + bit_map.set_bitv(Point2i(128, 128), true); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 1, "One bit should be set to true."); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 128)) == true, "The bit at (128,128) should be set to true"); + + bit_map.set_bitv(Point2i(128, 128), false); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "The bit should now be set to false again"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 128)) == false, "The bit at (128,128) should now be set to false again"); + + bit_map.create(dim); + bit_map.set_bitv(Point2i(512, 512), true); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "Nothing should change as we were trying to edit a bit outside of the correct range."); +} + +TEST_CASE("[BitMap] Get bit") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + + CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 128)) == false, "Trying to access a bit outside of the BitMap's range should always return false"); + + bit_map.create(dim); + CHECK(bit_map.get_bitv(Point2i(128, 128)) == false); + + bit_map.set_bit_rect(Rect2i(-1, -1, 257, 257), true); + + // Checking that range is [0, 256). + CHECK(bit_map.get_bitv(Point2i(-1, 0)) == false); + CHECK(bit_map.get_bitv(Point2i(0, 0)) == true); + CHECK(bit_map.get_bitv(Point2i(128, 128)) == true); + CHECK(bit_map.get_bitv(Point2i(255, 255)) == true); + CHECK(bit_map.get_bitv(Point2i(256, 256)) == false); + CHECK(bit_map.get_bitv(Point2i(257, 257)) == false); +} + +TEST_CASE("[BitMap] Set bit rect") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + + // Although we have not setup the BitMap yet, this should not crash because we get an empty intersection inside of the method. + bit_map.set_bit_rect(Rect2i{ 0, 0, 128, 128 }, true); + + bit_map.create(dim); + CHECK(bit_map.get_true_bit_count() == 0); + + bit_map.set_bit_rect(Rect2i{ 0, 0, 256, 256 }, true); + CHECK(bit_map.get_true_bit_count() == 65536); + + reset_bit_map(bit_map); + + // Checking out of bounds handling. + bit_map.set_bit_rect(Rect2i{ 128, 128, 256, 256 }, true); + CHECK(bit_map.get_true_bit_count() == 16384); + + reset_bit_map(bit_map); + + bit_map.set_bit_rect(Rect2i{ -128, -128, 256, 256 }, true); + CHECK(bit_map.get_true_bit_count() == 16384); + + reset_bit_map(bit_map); + + bit_map.set_bit_rect(Rect2i{ -128, -128, 512, 512 }, true); + CHECK(bit_map.get_true_bit_count() == 65536); +} + +TEST_CASE("[BitMap] Get true bit count") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + + CHECK(bit_map.get_true_bit_count() == 0); + + bit_map.create(dim); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "Uninitialized bit map should have no true bits"); + bit_map.set_bit_rect(Rect2i{ 0, 0, 256, 256 }, true); + CHECK(bit_map.get_true_bit_count() == 65536); + bit_map.set_bitv(Point2i{ 0, 0 }, false); + CHECK(bit_map.get_true_bit_count() == 65535); + bit_map.set_bit_rect(Rect2i{ 0, 0, 256, 256 }, false); + CHECK(bit_map.get_true_bit_count() == 0); +} + +TEST_CASE("[BitMap] Get size") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + + CHECK_MESSAGE(bit_map.get_size() == Size2i(0, 0), "Uninitialized bit map should have a size of 0x0"); + + bit_map.create(dim); + CHECK(bit_map.get_size() == Size2i(256, 256)); + + bit_map.create(Size2i(-1, 0)); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "Invalid size should not be accepted by create"); + + bit_map.create(Size2i(256, 128)); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 128), "Bitmap should have updated size"); +} + +TEST_CASE("[BitMap] Resize") { + const Size2i dim{ 128, 128 }; + BitMap bit_map{}; + + bit_map.resize(dim); + CHECK(bit_map.get_size() == dim); + + bit_map.create(dim); + bit_map.set_bit_rect(Rect2i(0, 0, 10, 10), true); + bit_map.set_bit_rect(Rect2i(118, 118, 10, 10), true); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 200, "There should be 100 bits in the top left corner, and 100 bits in the bottom right corner"); + bit_map.resize(Size2i(64, 64)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 50, "There should be 25 bits in the top left corner, and 25 bits in the bottom right corner"); + + bit_map.create(dim); + bit_map.resize(Size2i(-1, 128)); + CHECK_MESSAGE(bit_map.get_size() == Size2i(128, 128), "When an invalid size is given the bit map will keep its size"); + + bit_map.create(dim); + bit_map.set_bit_rect(Rect2i(0, 0, 10, 10), true); + bit_map.set_bit_rect(Rect2i(118, 118, 10, 10), true); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 200, "There should be 100 bits in the top left corner, and 100 bits in the bottom right corner"); + bit_map.resize(Size2i(256, 256)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 800, "There should still be 100 bits in the bottom right corner, and all new bits should be initialized to false"); + CHECK_MESSAGE(bit_map.get_size() == Size2i(256, 256), "The bitmap should now be 256x256"); +} + +TEST_CASE("[BitMap] Grow and shrink mask") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + bit_map.grow_mask(100, Rect2i(0, 0, 128, 128)); // Check if method does not crash when working with an uninitialised bit map. + CHECK_MESSAGE(bit_map.get_size() == Size2i(0, 0), "Size should still be equal to 0x0"); + + bit_map.create(dim); + + bit_map.set_bit_rect(Rect2i(96, 96, 64, 64), true); + + CHECK_MESSAGE(bit_map.get_true_bit_count() == 4096, "Creating a square of 64x64 should be 4096 bits"); + bit_map.grow_mask(0, Rect2i(0, 0, 256, 256)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 4096, "Growing with size of 0 should not change any bits"); + + reset_bit_map(bit_map); + + bit_map.set_bit_rect(Rect2i(96, 96, 64, 64), true); + + CHECK_MESSAGE(bit_map.get_bitv(Point2i(95, 128)) == false, "Bits just outside of the square should not be set"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(160, 128)) == false, "Bits just outside of the square should not be set"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 95)) == false, "Bits just outside of the square should not be set"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 160)) == false, "Bits just outside of the square should not be set"); + bit_map.grow_mask(1, Rect2i(0, 0, 256, 256)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 4352, "We should have 4*64 (perimeter of square) more bits set to true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(95, 128)) == true, "Bits that were just outside of the square should now be set to true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(160, 128)) == true, "Bits that were just outside of the square should now be set to true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 95)) == true, "Bits that were just outside of the square should now be set to true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(128, 160)) == true, "Bits that were just outside of the square should now be set to true"); + + reset_bit_map(bit_map); + + bit_map.set_bit_rect(Rect2i(127, 127, 1, 1), true); + + CHECK(bit_map.get_true_bit_count() == 1); + bit_map.grow_mask(32, Rect2i(0, 0, 256, 256)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 3209, "Creates a circle around the initial bit with a radius of 32 bits. Any bit that has a distance within this radius will be set to true"); + + reset_bit_map(bit_map); + + bit_map.set_bit_rect(Rect2i(127, 127, 1, 1), true); + for (int i = 0; i < 32; i++) { + bit_map.grow_mask(1, Rect2i(0, 0, 256, 256)); + } + CHECK_MESSAGE(bit_map.get_true_bit_count() == 2113, "Creates a diamond around the initial bit with diagonals that are 65 bits long."); + + reset_bit_map(bit_map); + + bit_map.set_bit_rect(Rect2i(123, 123, 10, 10), true); + + CHECK(bit_map.get_true_bit_count() == 100); + bit_map.grow_mask(-11, Rect2i(0, 0, 256, 256)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 0, "Shrinking by more than the width of the square should totally remove it."); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(96, 96, 64, 64), true); + + CHECK_MESSAGE(bit_map.get_bitv(Point2i(96, 129)) == true, "Bits on the edge of the square should be true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(159, 129)) == true, "Bits on the edge of the square should be true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 96)) == true, "Bits on the edge of the square should be true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 159)) == true, "Bits on the edge of the square should be true"); + bit_map.grow_mask(-1, Rect2i(0, 0, 256, 256)); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 3844, "Shrinking by 1 should set 4*63=252 bits to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(96, 129)) == false, "Bits that were on the edge of the square should now be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(159, 129)) == false, "Bits that were on the edge of the square should now be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 96)) == false, "Bits that were on the edge of the square should now be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(129, 159)) == false, "Bits that were on the edge of the square should now be set to false"); + + reset_bit_map(bit_map); + + bit_map.set_bit_rect(Rect2i(125, 125, 1, 6), true); + bit_map.set_bit_rect(Rect2i(130, 125, 1, 6), true); + bit_map.set_bit_rect(Rect2i(125, 130, 6, 1), true); + + CHECK(bit_map.get_true_bit_count() == 16); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 131)) == false, "Bits that are on the edge of the shape should be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(131, 131)) == false, "Bits that are on the edge of the shape should be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 124)) == false, "Bits that are on the edge of the shape should be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(130, 124)) == false, "Bits that are on the edge of the shape should be set to false"); + bit_map.grow_mask(1, Rect2i(0, 0, 256, 256)); + CHECK(bit_map.get_true_bit_count() == 48); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 131)) == true, "Bits that were on the edge of the shape should now be set to true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(131, 130)) == true, "Bits that were on the edge of the shape should now be set to true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(125, 124)) == true, "Bits that were on the edge of the shape should now be set to true"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(130, 124)) == true, "Bits that were on the edge of the shape should now be set to true"); + + CHECK_MESSAGE(bit_map.get_bitv(Point2i(124, 124)) == false, "Bits that are on the edge of the shape should be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(126, 124)) == false, "Bits that are on the edge of the shape should be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(124, 131)) == false, "Bits that are on the edge of the shape should be set to false"); + CHECK_MESSAGE(bit_map.get_bitv(Point2i(131, 131)) == false, "Bits that are on the edge of the shape should be set to false"); +} + +TEST_CASE("[BitMap] Blit") { + Point2i blit_pos{ 128, 128 }; + Point2i bit_map_size{ 256, 256 }; + Point2i blit_size{ 32, 32 }; + + BitMap bit_map{}; + Ref<BitMap> blit_bit_map{}; + + // Testing null reference to blit bit map. + bit_map.blit(blit_pos, blit_bit_map); + + blit_bit_map.instantiate(); + + // Testing if uninitialised blit bit map and uninitialised bit map does not crash + bit_map.blit(blit_pos, blit_bit_map); + + // Testing if uninitialised bit map does not crash + blit_bit_map->create(blit_size); + bit_map.blit(blit_pos, blit_bit_map); + + // Testing if uninitialised bit map does not crash + blit_bit_map.unref(); + blit_bit_map.instantiate(); + CHECK_MESSAGE(blit_bit_map->get_size() == Point2i(0, 0), "Size should be cleared by unref and instance calls."); + bit_map.create(bit_map_size); + bit_map.blit(Point2i(128, 128), blit_bit_map); + + // Testing if both initialised does not crash. + blit_bit_map->create(blit_size); + bit_map.blit(blit_pos, blit_bit_map); + + bit_map.set_bit_rect(Rect2i{ 127, 127, 3, 3 }, true); + CHECK(bit_map.get_true_bit_count() == 9); + bit_map.blit(Point2i(112, 112), blit_bit_map); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 9, "No bits should have been changed, as the blit bit map only contains falses"); + + bit_map.create(bit_map_size); + blit_bit_map->create(blit_size); + blit_bit_map->set_bit_rect(Rect2i(15, 15, 3, 3), true); + CHECK(blit_bit_map->get_true_bit_count() == 9); + + CHECK(bit_map.get_true_bit_count() == 0); + bit_map.blit(Point2i(112, 112), blit_bit_map); + CHECK_MESSAGE(bit_map.get_true_bit_count() == 9, "All true bits should have been moved to the bit map"); + for (int x = 127; x < 129; ++x) { + for (int y = 127; y < 129; ++y) { + CHECK_MESSAGE(bit_map.get_bitv(Point2i(x, y)) == true, "All true bits should have been moved to the bit map"); + } + } +} + +TEST_CASE("[BitMap] Convert to image") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + Ref<Image> img; + + img = bit_map.convert_to_image(); + CHECK_MESSAGE(img.is_valid(), "We should receive a valid Image Object even if BitMap is not created yet"); + CHECK_MESSAGE(img->get_format() == Image::FORMAT_L8, "We should receive a valid Image Object even if BitMap is not created yet"); + CHECK_MESSAGE(img->get_size() == (Size2i(0, 0)), "Image should have no width or height, because BitMap has not yet been created"); + + bit_map.create(dim); + img = bit_map.convert_to_image(); + CHECK_MESSAGE(img->get_size() == dim, "Image should have the same dimensions as the BitMap"); + CHECK_MESSAGE(img->get_pixel(0, 0).is_equal_approx(Color(0, 0, 0)), "BitMap is initialized to all 0's, so Image should be all black"); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(0, 0, 128, 128), true); + img = bit_map.convert_to_image(); + CHECK_MESSAGE(img->get_pixel(0, 0).is_equal_approx(Color(1, 1, 1)), "BitMap's top-left quadrant is all 1's, so Image should be white"); + CHECK_MESSAGE(img->get_pixel(256, 256).is_equal_approx(Color(0, 0, 0)), "All other quadrants were 0's, so these should be black"); +} + +TEST_CASE("[BitMap] Clip to polygon") { + const Size2i dim{ 256, 256 }; + BitMap bit_map{}; + Vector<Vector<Vector2>> polygons; + + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128)); + CHECK_MESSAGE(polygons.size() == 0, "We should have no polygons, because the BitMap was not initialized"); + + bit_map.create(dim); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128)); + CHECK_MESSAGE(polygons.size() == 0, "We should have no polygons, because the BitMap was all 0's"); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(0, 0, 64, 64), true); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128)); + CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon"); + CHECK_MESSAGE(polygons[0].size() == 4, "The polygon should have exactly 4 points"); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(0, 0, 32, 32), true); + bit_map.set_bit_rect(Rect2i(64, 64, 32, 32), true); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128)); + CHECK_MESSAGE(polygons.size() == 2, "We should have exactly 2 polygons"); + CHECK_MESSAGE(polygons[0].size() == 4, "The polygon should have exactly 4 points"); + CHECK_MESSAGE(polygons[1].size() == 4, "The polygon should have exactly 4 points"); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(124, 112, 8, 32), true); + bit_map.set_bit_rect(Rect2i(112, 124, 32, 8), true); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 256, 256)); + CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon"); + CHECK_MESSAGE(polygons[0].size() == 12, "The polygon should have exactly 12 points"); + + reset_bit_map(bit_map); + bit_map.set_bit_rect(Rect2i(124, 112, 8, 32), true); + bit_map.set_bit_rect(Rect2i(112, 124, 32, 8), true); + polygons = bit_map.clip_opaque_to_polygons(Rect2i(0, 0, 128, 128)); + CHECK_MESSAGE(polygons.size() == 1, "We should have exactly 1 polygon"); + CHECK_MESSAGE(polygons[0].size() == 6, "The polygon should have exactly 6 points"); +} + +} // namespace TestBitmap + +#endif // TEST_BIT_MAP_H diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h new file mode 100644 index 0000000000..3940bdb37a --- /dev/null +++ b/tests/scene/test_code_edit.h @@ -0,0 +1,3393 @@ +/*************************************************************************/ +/* test_code_edit.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_CODE_EDIT_H +#define TEST_CODE_EDIT_H + +#include "scene/gui/code_edit.h" + +#include "tests/test_macros.h" + +namespace TestCodeEdit { + +TEST_CASE("[SceneTree][CodeEdit] line gutters") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + SUBCASE("[CodeEdit] breakpoints") { + SIGNAL_WATCH(code_edit, "breakpoint_toggled"); + + SUBCASE("[CodeEdit] draw breakpoints gutter") { + code_edit->set_draw_breakpoints_gutter(false); + CHECK_FALSE(code_edit->is_drawing_breakpoints_gutter()); + + code_edit->set_draw_breakpoints_gutter(true); + CHECK(code_edit->is_drawing_breakpoints_gutter()); + } + + SUBCASE("[CodeEdit] set line as breakpoint") { + /* Out of bounds. */ + ERR_PRINT_OFF; + + code_edit->set_line_as_breakpoint(-1, true); + CHECK_FALSE(code_edit->is_line_breakpointed(-1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + code_edit->set_line_as_breakpoint(1, true); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + ERR_PRINT_ON; + + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + CHECK(code_edit->get_breakpointed_lines()[0] == 0); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_line_as_breakpoint(0, false); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] clear breakpointed lines") { + code_edit->clear_breakpointed_lines(); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->clear_breakpointed_lines(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and set text") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* breakpoint on lines that still exist are kept. */ + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* breakpoint on lines that are removed should also be removed. */ + code_edit->clear_breakpointed_lines(); + SIGNAL_DISCARD("breakpoint_toggled") + + ((Array)args[0])[0] = 1; + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and clear") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* breakpoint on lines that still exist are removed. */ + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* breakpoint on lines that are removed should also be removed. */ + code_edit->clear_breakpointed_lines(); + SIGNAL_DISCARD("breakpoint_toggled") + + ((Array)args[0])[0] = 1; + code_edit->set_text("test\nline"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and new lines no text") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + /* No text moves breakpoint. */ + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Normal. */ + ((Array)args[0])[0] = 0; + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Non-Breaking. */ + ((Array)args[0])[0] = 1; + ((Array)args[1])[0] = 2; + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + CHECK(code_edit->is_line_breakpointed(2)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Above. */ + ((Array)args[0])[0] = 2; + ((Array)args[1])[0] = 3; + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_breakpointed(2)); + CHECK(code_edit->is_line_breakpointed(3)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and new lines with text") { + Array arg1; + arg1.push_back(0); + Array args; + args.push_back(arg1); + + /* Having text does not move breakpoint. */ + code_edit->insert_text_at_caret("text"); + code_edit->set_line_as_breakpoint(0, true); + CHECK(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_breakpointed(0)); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* Non-Breaking. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK(code_edit->is_line_breakpointed(0)); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* Above does move. */ + ((Array)args[0])[0] = 0; + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and backspace") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_caret_line(2); + + /* backspace onto line does not remove breakpoint */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* backspace on breakpointed line removes it */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Backspace above breakpointed line moves it. */ + ((Array)args[0])[0] = 2; + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(2, true); + CHECK(code_edit->is_line_breakpointed(2)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_caret_line(1); + + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(2)); + ERR_PRINT_ON; + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and delete") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + code_edit->set_caret_line(1); + + /* Delete onto breakpointed lines does not remove it. */ + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + + /* Delete moving breakpointed line up removes it. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 1); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + ERR_PRINT_ON; + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Delete above breakpointed line moves it. */ + ((Array)args[0])[0] = 2; + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(2, true); + CHECK(code_edit->is_line_breakpointed(2)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->set_caret_line(0); + + Array arg2; + arg2.push_back(1); + args.push_back(arg2); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(2)); + ERR_PRINT_ON; + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and delete selection") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Should handle breakpoint move when deleting selection by adding less text then removed. */ + ((Array)args[0])[0] = 9; + + code_edit->set_text("\n\n\n\n\n\n\n\n\n"); + code_edit->set_line_as_breakpoint(9, true); + CHECK(code_edit->is_line_breakpointed(9)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->select(0, 0, 6, 0); + + Array arg2; + arg2.push_back(4); + args.push_back(arg2); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_breakpointed(9)); + ERR_PRINT_ON; + CHECK(code_edit->is_line_breakpointed(4)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Should handle breakpoint move when deleting selection by adding more text then removed. */ + ((Array)args[0])[0] = 9; + ((Array)args[1])[0] = 14; + + code_edit->insert_text_at_caret("\n\n\n\n\n"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("breakpoint_toggled") + CHECK(code_edit->is_line_breakpointed(9)); + + code_edit->select(0, 0, 6, 0); + code_edit->insert_text_at_caret("\n\n\n\n\n\n\n\n\n\n\n"); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_breakpointed(14)); + SIGNAL_CHECK("breakpoint_toggled", args); + } + + SUBCASE("[CodeEdit] breakpoints and undo") { + Array arg1; + arg1.push_back(1); + Array args; + args.push_back(arg1); + + code_edit->set_text("\n\n"); + code_edit->set_line_as_breakpoint(1, true); + CHECK(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK("breakpoint_toggled", args); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_breakpointed(0)); + SIGNAL_CHECK("breakpoint_toggled", args); + + /* Undo does not restore breakpoint. */ + code_edit->undo(); + CHECK_FALSE(code_edit->is_line_breakpointed(1)); + SIGNAL_CHECK_FALSE("breakpoint_toggled"); + } + + SIGNAL_UNWATCH(code_edit, "breakpoint_toggled"); + } + + SUBCASE("[CodeEdit] bookmarks") { + SUBCASE("[CodeEdit] draw bookmarks gutter") { + code_edit->set_draw_bookmarks_gutter(false); + CHECK_FALSE(code_edit->is_drawing_bookmarks_gutter()); + + code_edit->set_draw_bookmarks_gutter(true); + CHECK(code_edit->is_drawing_bookmarks_gutter()); + } + + SUBCASE("[CodeEdit] set line as bookmarks") { + /* Out of bounds. */ + ERR_PRINT_OFF; + + code_edit->set_line_as_bookmarked(-1, true); + CHECK_FALSE(code_edit->is_line_bookmarked(-1)); + + code_edit->set_line_as_bookmarked(1, true); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + + ERR_PRINT_ON; + + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->get_bookmarked_lines()[0] == 0); + CHECK(code_edit->is_line_bookmarked(0)); + + code_edit->set_line_as_bookmarked(0, false); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + } + + SUBCASE("[CodeEdit] clear bookmarked lines") { + code_edit->clear_bookmarked_lines(); + + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + code_edit->clear_bookmarked_lines(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + } + + SUBCASE("[CodeEdit] bookmarks and set text") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that still exist are kept. */ + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that are removed should also be removed. */ + code_edit->clear_bookmarked_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and clear") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that still exist are removed. */ + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + + /* bookmarks on lines that are removed should also be removed. */ + code_edit->clear_bookmarked_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and new lines no text") { + /* No text moves bookmarks. */ + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + CHECK(code_edit->is_line_bookmarked(1)); + + /* Non-Breaking. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + CHECK(code_edit->is_line_bookmarked(2)); + + /* Above. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_bookmarked(2)); + CHECK(code_edit->is_line_bookmarked(3)); + } + + SUBCASE("[CodeEdit] bookmarks and new lines with text") { + /* Having text does not move bookmark. */ + code_edit->insert_text_at_caret("text"); + code_edit->set_line_as_bookmarked(0, true); + CHECK(code_edit->is_line_bookmarked(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_bookmarked(0)); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + + /* Non-Breaking. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK(code_edit->is_line_bookmarked(0)); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + + /* Above does move. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + CHECK(code_edit->is_line_bookmarked(1)); + } + + SUBCASE("[CodeEdit] bookmarks and backspace") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->set_caret_line(2); + + /* backspace onto line does not remove bookmark */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->is_line_bookmarked(1)); + + /* backspace on bookmarked line removes it */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and delete") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + code_edit->set_caret_line(1); + + /* Delete onto bookmarked lines does not remove it. */ + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_bookmarked(1)); + + /* Delete moving bookmarked line up removes it. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 1); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] bookmarks and delete selection") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + } + + SUBCASE("[CodeEdit] bookmarks and undo") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_bookmarked(1, true); + CHECK(code_edit->is_line_bookmarked(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_bookmarked(0)); + + /* Undo does not restore bookmark. */ + code_edit->undo(); + CHECK_FALSE(code_edit->is_line_bookmarked(1)); + } + } + + SUBCASE("[CodeEdit] executing lines") { + SUBCASE("[CodeEdit] draw executing lines gutter") { + code_edit->set_draw_executing_lines_gutter(false); + CHECK_FALSE(code_edit->is_drawing_executing_lines_gutter()); + + code_edit->set_draw_executing_lines_gutter(true); + CHECK(code_edit->is_drawing_executing_lines_gutter()); + } + + SUBCASE("[CodeEdit] set line as executing lines") { + /* Out of bounds. */ + ERR_PRINT_OFF; + + code_edit->set_line_as_executing(-1, true); + CHECK_FALSE(code_edit->is_line_executing(-1)); + + code_edit->set_line_as_executing(1, true); + CHECK_FALSE(code_edit->is_line_executing(1)); + + ERR_PRINT_ON; + + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->get_executing_lines()[0] == 0); + CHECK(code_edit->is_line_executing(0)); + + code_edit->set_line_as_executing(0, false); + CHECK_FALSE(code_edit->is_line_executing(0)); + } + + SUBCASE("[CodeEdit] clear executing lines lines") { + code_edit->clear_executing_lines(); + + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + code_edit->clear_executing_lines(); + CHECK_FALSE(code_edit->is_line_executing(0)); + } + + SUBCASE("[CodeEdit] executing lines and set text") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* executing on lines that still exist are kept. */ + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK(code_edit->is_line_executing(0)); + + /* executing on lines that are removed should also be removed. */ + code_edit->clear_executing_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->set_text(""); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and clear") { + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* executing on lines that still exist are removed. */ + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + + /* executing on lines that are removed should also be removed. */ + code_edit->clear_executing_lines(); + + code_edit->set_text("test\nline"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and new lines no text") { + /* No text moves executing lines. */ + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK_FALSE(code_edit->is_line_executing(0)); + CHECK(code_edit->is_line_executing(1)); + + /* Non-Breaking. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK_FALSE(code_edit->is_line_executing(1)); + CHECK(code_edit->is_line_executing(2)); + + /* Above. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_executing(2)); + CHECK(code_edit->is_line_executing(3)); + } + + SUBCASE("[CodeEdit] executing lines and new lines with text") { + /* Having text does not move executing lines. */ + code_edit->insert_text_at_caret("text"); + code_edit->set_line_as_executing(0, true); + CHECK(code_edit->is_line_executing(0)); + + /* Normal. */ + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_executing(0)); + CHECK_FALSE(code_edit->is_line_executing(1)); + + /* Non-Breaking. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line_count() == 3); + CHECK(code_edit->is_line_executing(0)); + CHECK_FALSE(code_edit->is_line_executing(1)); + + /* Above does move. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line_count() == 4); + CHECK_FALSE(code_edit->is_line_executing(0)); + CHECK(code_edit->is_line_executing(1)); + } + + SUBCASE("[CodeEdit] executing lines and backspace") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->set_caret_line(2); + + /* backspace onto line does not remove executing lines. */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->is_line_executing(1)); + + /* backspace on executing line removes it */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK_FALSE(code_edit->is_line_executing(0)); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and delete") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + code_edit->set_caret_line(1); + + /* Delete onto executing lines does not remove it. */ + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 2); + CHECK(code_edit->is_line_executing(1)); + + /* Delete moving executing line up removes it. */ + code_edit->set_caret_line(0); + SEND_GUI_ACTION(code_edit, "ui_text_delete"); + CHECK(code_edit->get_line_count() == 1); + ERR_PRINT_OFF; + CHECK_FALSE(code_edit->is_line_executing(1)); + ERR_PRINT_ON; + } + + SUBCASE("[CodeEdit] executing lines and delete selection") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + } + + SUBCASE("[CodeEdit] executing lines and undo") { + code_edit->set_text("\n\n"); + code_edit->set_line_as_executing(1, true); + CHECK(code_edit->is_line_executing(1)); + + code_edit->select(0, 0, 2, 0); + code_edit->delete_selection(); + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(code_edit->is_line_executing(0)); + + /* Undo does not restore executing lines. */ + code_edit->undo(); + CHECK_FALSE(code_edit->is_line_executing(1)); + } + } + + SUBCASE("[CodeEdit] line numbers") { + SUBCASE("[CodeEdit] draw line numbers gutter and padding") { + code_edit->set_draw_line_numbers(false); + CHECK_FALSE(code_edit->is_draw_line_numbers_enabled()); + + code_edit->set_draw_line_numbers(true); + CHECK(code_edit->is_draw_line_numbers_enabled()); + + code_edit->set_line_numbers_zero_padded(false); + CHECK_FALSE(code_edit->is_line_numbers_zero_padded()); + + code_edit->set_line_numbers_zero_padded(true); + CHECK(code_edit->is_line_numbers_zero_padded()); + + code_edit->set_line_numbers_zero_padded(false); + CHECK_FALSE(code_edit->is_line_numbers_zero_padded()); + + code_edit->set_draw_line_numbers(false); + CHECK_FALSE(code_edit->is_draw_line_numbers_enabled()); + + code_edit->set_line_numbers_zero_padded(true); + CHECK(code_edit->is_line_numbers_zero_padded()); + } + } + + SUBCASE("[CodeEdit] line folding") { + SUBCASE("[CodeEdit] draw line folding gutter") { + code_edit->set_draw_fold_gutter(false); + CHECK_FALSE(code_edit->is_drawing_fold_gutter()); + + code_edit->set_draw_fold_gutter(true); + CHECK(code_edit->is_drawing_fold_gutter()); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] delimiters") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + const Point2 OUTSIDE_DELIMETER = Point2(-1, -1); + + code_edit->clear_string_delimiters(); + code_edit->clear_comment_delimiters(); + + SUBCASE("[CodeEdit] add and remove delimiters") { + SUBCASE("[CodeEdit] add and remove string delimiters") { + /* Add a delimiter.*/ + code_edit->add_string_delimiter("\"", "\"", false); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Adding a duplicate start key is not allowed. */ + code_edit->add_string_delimiter("\"", "\'", false); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Adding a duplicate end key is allowed. */ + code_edit->add_string_delimiter("'", "\"", false); + CHECK(code_edit->has_string_delimiter("'")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Both start and end keys have to be symbols. */ + code_edit->add_string_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("@", "f", false); + CHECK_FALSE(code_edit->has_string_delimiter("@")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->add_string_delimiter("f", "f", false); + CHECK_FALSE(code_edit->has_string_delimiter("f")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Blank start keys are not allowed */ + code_edit->add_string_delimiter("", "#", false); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + ERR_PRINT_ON; + + /* Blank end keys are allowed. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 3); + + /* Remove a delimiter. */ + code_edit->remove_string_delimiter("#"); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + /* Set should override existing, and test multiline */ + TypedArray<String> delimiters; + delimiters.push_back("^^ ^^"); + + code_edit->set_string_delimiters(delimiters); + CHECK_FALSE(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->has_string_delimiter("^^")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* clear should remove all. */ + code_edit->clear_string_delimiters(); + CHECK_FALSE(code_edit->has_string_delimiter("^^")); + CHECK(code_edit->get_string_delimiters().size() == 0); + } + + SUBCASE("[CodeEdit] add and remove comment delimiters") { + /* Add a delimiter.*/ + code_edit->add_comment_delimiter("\"", "\"", false); + CHECK(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Adding a duplicate start key is not allowed. */ + code_edit->add_comment_delimiter("\"", "\'", false); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Adding a duplicate end key is allowed. */ + code_edit->add_comment_delimiter("'", "\"", false); + CHECK(code_edit->has_comment_delimiter("'")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Both start and end keys have to be symbols. */ + code_edit->add_comment_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("f", "\"", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("@", "f", false); + CHECK_FALSE(code_edit->has_comment_delimiter("@")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->add_comment_delimiter("f", "f", false); + CHECK_FALSE(code_edit->has_comment_delimiter("f")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Blank start keys are not allowed. */ + code_edit->add_comment_delimiter("", "#", false); + CHECK_FALSE(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + ERR_PRINT_ON; + + /* Blank end keys are allowed. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 3); + + /* Remove a delimiter. */ + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + /* Set should override existing, and test multiline. */ + TypedArray<String> delimiters; + delimiters.push_back("^^ ^^"); + + code_edit->set_comment_delimiters(delimiters); + CHECK_FALSE(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->has_comment_delimiter("^^")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* clear should remove all. */ + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->has_comment_delimiter("^^")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + } + + SUBCASE("[CodeEdit] add and remove mixed delimiters") { + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_OFF; + + /* Disallow adding a string with the same start key as comment. */ + code_edit->add_string_delimiter("#", "", false); + CHECK_FALSE(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + code_edit->add_string_delimiter("\"", "\"", false); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Disallow adding a comment with the same start key as string. */ + code_edit->add_comment_delimiter("\"", "", false); + CHECK_FALSE(code_edit->has_comment_delimiter("\"")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + ERR_PRINT_ON; + + /* Cannot remove string with remove comment. */ + code_edit->remove_comment_delimiter("\""); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Cannot remove comment with remove string. */ + code_edit->remove_string_delimiter("#"); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Clear comments leave strings. */ + code_edit->clear_comment_delimiters(); + CHECK(code_edit->has_string_delimiter("\"")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Clear string leave comments. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + code_edit->clear_string_delimiters(); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + } + } + + SUBCASE("[CodeEdit] single line delimiters") { + SUBCASE("[CodeEdit] single line string delimiters") { + /* Blank end key should set lineonly to true. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Insert line above, line with string then line below. */ + code_edit->insert_text_at_caret(" \n#\n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after start key is in string and start / end positions are correct. */ + CHECK(code_edit->is_in_string(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1)); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check region metadata. */ + int idx = code_edit->is_in_string(1, 1); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == ""); + + /* Check nested strings are handled correctly. */ + code_edit->set_text(" \n# # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in string and start / end positions are correct. */ + CHECK(code_edit->is_in_string(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first. */ + CHECK(code_edit->is_in_string(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check is in string with no column returns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # # \n "); + CHECK(code_edit->is_in_string(1) != -1); + + code_edit->set_text(" \n text # # \n "); + CHECK(code_edit->is_in_string(1) == -1); + + /* Removing delimiter should update. */ + code_edit->set_text(" \n # # \n "); + + code_edit->remove_string_delimiter("#"); + CHECK_FALSE(code_edit->has_string_delimiter("$")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + CHECK(code_edit->is_in_string(1) == -1); + + /* Adding and clear should update. */ + code_edit->add_string_delimiter("#", "", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + CHECK(code_edit->is_in_string(1) != -1); + + code_edit->clear_string_delimiters(); + CHECK_FALSE(code_edit->has_string_delimiter("$")); + CHECK(code_edit->get_string_delimiters().size() == 0); + + CHECK(code_edit->is_in_string(1) == -1); + } + + SUBCASE("[CodeEdit] single line comment delimiters") { + /* Blank end key should set lineonly to true. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Insert line above, line with comment then line below. */ + code_edit->insert_text_at_caret(" \n#\n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(2, 1)); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check region metadata. */ + int idx = code_edit->is_in_comment(1, 1); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == ""); + + /* Check nested comments are handled correctly. */ + code_edit->set_text(" \n# # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first. */ + CHECK(code_edit->is_in_comment(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check is in comment with no column returns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # # \n "); + CHECK(code_edit->is_in_comment(1) != -1); + + code_edit->set_text(" \n text # # \n "); + CHECK(code_edit->is_in_comment(1) == -1); + + /* Removing delimiter should update. */ + code_edit->set_text(" \n # # \n "); + + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + CHECK(code_edit->is_in_comment(1) == -1); + + /* Adding and clear should update. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + CHECK(code_edit->is_in_comment(1) != -1); + + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + CHECK(code_edit->is_in_comment(1) == -1); + } + + SUBCASE("[CodeEdit] single line mixed delimiters") { + /* Blank end key should set lineonly to true. */ + /* Add string delimiter. */ + code_edit->add_string_delimiter("&", "", false); + CHECK(code_edit->has_string_delimiter("&")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Nest a string delimiter inside a comment. */ + code_edit->set_text(" \n# & \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before first start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column after the first start key is in comment and start / end positions are correct. */ + CHECK(code_edit->is_in_comment(1, 1) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 1) == Point2(6, 1)); + + /* Check column after the second start key returns data for the first, and does not state string. */ + CHECK(code_edit->is_in_comment(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(1, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + CHECK(code_edit->is_in_string(1, 5) == -1); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Remove the comment delimiter. */ + code_edit->remove_comment_delimiter("#"); + CHECK_FALSE(code_edit->has_comment_delimiter("$")); + CHECK(code_edit->get_comment_delimiters().size() == 0); + + /* The "first" comment region is no longer valid. */ + CHECK(code_edit->is_in_comment(1, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 1) == OUTSIDE_DELIMETER); + + /* The "second" region as string is now valid. */ + CHECK(code_edit->is_in_string(1, 5) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 5) == Point2(4, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 5) == Point2(6, 1)); + } + } + + SUBCASE("[CodeEdit] multiline delimiters") { + SUBCASE("[CodeEdit] multiline string delimiters") { + code_edit->clear_string_delimiters(); + code_edit->clear_comment_delimiters(); + + /* Add string delimiter. */ + code_edit->add_string_delimiter("#", "#", false); + CHECK(code_edit->has_string_delimiter("#")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* First test over a single line. */ + code_edit->set_text(" \n # # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column before closing delimiter is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(1, 6) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check the region metadata. */ + int idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test over a multiple blank lines. */ + code_edit->set_text(" \n # \n\n # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check blank middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* Next test over a multiple non-blank lines. */ + code_edit->set_text(" \n # \n \n # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test nested strings. */ + code_edit->add_string_delimiter("^", "^", false); + CHECK(code_edit->has_string_delimiter("^")); + CHECK(code_edit->get_string_delimiters().size() == 2); + + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in string. */ + CHECK(code_edit->is_in_string(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in string. */ + CHECK(code_edit->is_in_string(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in string. */ + CHECK(code_edit->is_in_string(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_string(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in string. */ + CHECK(code_edit->is_in_string(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in string. */ + CHECK(code_edit->is_in_string(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in string. */ + CHECK(code_edit->is_in_string(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test no end key. */ + code_edit->set_text(" \n # \n "); + + /* check the region metadata. */ + idx = code_edit->is_in_string(1, 2); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1)); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in string with no column returns true if entire line is string excluding whitespace. */ + code_edit->set_text(" \n # \n\n #\n "); + CHECK(code_edit->is_in_string(1) != -1); + CHECK(code_edit->is_in_string(2) != -1); + CHECK(code_edit->is_in_string(3) != -1); + + code_edit->set_text(" \n test # \n\n # test \n "); + CHECK(code_edit->is_in_string(1) == -1); + CHECK(code_edit->is_in_string(2) != -1); + CHECK(code_edit->is_in_string(3) == -1); + } + + SUBCASE("[CodeEdit] multiline comment delimiters") { + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "#", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* First test over a single line. */ + code_edit->set_text(" \n # # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column before closing delimiter is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(5, 1)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 6) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 6) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 6) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(2, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(2, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(2, 1) == OUTSIDE_DELIMETER); + + /* Check the region metadata. */ + int idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test over a multiple blank lines. */ + code_edit->set_text(" \n # \n\n # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check blank middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* Next test over a multiple non-blank lines. */ + code_edit->set_text(" \n # \n \n # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(2, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(2, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(2, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test nested comments. */ + code_edit->add_comment_delimiter("^", "^", false); + CHECK(code_edit->has_comment_delimiter("^")); + CHECK(code_edit->get_comment_delimiters().size() == 2); + + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Next test no end key. */ + code_edit->set_text(" \n # \n "); + + /* check the region metadata. */ + idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(-1, -1)); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in comment with no column returns true if entire line is comment excluding whitespace. */ + code_edit->set_text(" \n # \n\n #\n "); + CHECK(code_edit->is_in_comment(1) != -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) != -1); + + code_edit->set_text(" \n test # \n\n # test \n "); + CHECK(code_edit->is_in_comment(1) == -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) == -1); + } + + SUBCASE("[CodeEdit] multiline mixed delimiters") { + /* Add comment delimiter. */ + code_edit->add_comment_delimiter("#", "#", false); + CHECK(code_edit->has_comment_delimiter("#")); + CHECK(code_edit->get_comment_delimiters().size() == 1); + + /* Add string delimiter. */ + code_edit->add_string_delimiter("^", "^", false); + CHECK(code_edit->has_string_delimiter("^")); + CHECK(code_edit->get_string_delimiters().size() == 1); + + /* Nest a string inside a comment. */ + code_edit->set_text(" \n # ^\n \n^ # \n "); + + /* Check line above is not in comment. */ + CHECK(code_edit->is_in_comment(0, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(0, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(0, 1) == OUTSIDE_DELIMETER); + + /* Check column before start key is not in comment. */ + CHECK(code_edit->is_in_comment(1, 0) == -1); + CHECK(code_edit->get_delimiter_start_position(1, 0) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(1, 0) == OUTSIDE_DELIMETER); + + /* Check column just after start key is in comment. */ + CHECK(code_edit->is_in_comment(1, 2) != -1); + CHECK(code_edit->get_delimiter_start_position(1, 2) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(1, 2) == Point2(3, 3)); + + /* Check middle line. */ + CHECK(code_edit->is_in_comment(2, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(2, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(2, 0) == Point2(3, 3)); + + /* Check column just before end key is in comment. */ + CHECK(code_edit->is_in_comment(3, 0) != -1); + CHECK(code_edit->get_delimiter_start_position(3, 0) == Point2(2, 1)); + CHECK(code_edit->get_delimiter_end_position(3, 0) == Point2(3, 3)); + + /* Check column after end key is not in comment. */ + CHECK(code_edit->is_in_comment(3, 3) == -1); + CHECK(code_edit->get_delimiter_start_position(3, 3) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(3, 3) == OUTSIDE_DELIMETER); + + /* Check line after is not in comment. */ + CHECK(code_edit->is_in_comment(4, 1) == -1); + CHECK(code_edit->get_delimiter_start_position(4, 1) == OUTSIDE_DELIMETER); + CHECK(code_edit->get_delimiter_end_position(4, 1) == OUTSIDE_DELIMETER); + + /* check the region metadata. */ + int idx = code_edit->is_in_comment(1, 2); + CHECK(code_edit->get_delimiter_start_key(idx) == "#"); + CHECK(code_edit->get_delimiter_end_key(idx) == "#"); + + /* Check is in comment with no column returns true as inner delimiter should not be counted. */ + CHECK(code_edit->is_in_comment(1) != -1); + CHECK(code_edit->is_in_comment(2) != -1); + CHECK(code_edit->is_in_comment(3) != -1); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] indent") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + SUBCASE("[CodeEdit] indent settings") { + code_edit->set_indent_size(10); + CHECK(code_edit->get_indent_size() == 10); + CHECK(code_edit->get_tab_size() == 10); + + code_edit->set_auto_indent_enabled(false); + CHECK_FALSE(code_edit->is_auto_indent_enabled()); + + code_edit->set_auto_indent_enabled(true); + CHECK(code_edit->is_auto_indent_enabled()); + + code_edit->set_indent_using_spaces(false); + CHECK_FALSE(code_edit->is_indent_using_spaces()); + + code_edit->set_indent_using_spaces(true); + CHECK(code_edit->is_indent_using_spaces()); + + /* Only the first char is registered. */ + TypedArray<String> auto_indent_prefixes; + auto_indent_prefixes.push_back("::"); + auto_indent_prefixes.push_back("s"); + auto_indent_prefixes.push_back("1"); + code_edit->set_auto_indent_prefixes(auto_indent_prefixes); + + auto_indent_prefixes = code_edit->get_auto_indent_prefixes(); + CHECK(auto_indent_prefixes.has(":")); + CHECK(auto_indent_prefixes.has("s")); + CHECK(auto_indent_prefixes.has("1")); + } + + SUBCASE("[CodeEdit] indent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Do nothing if not editable. */ + code_edit->set_editable(false); + + code_edit->do_indent(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->indent_lines(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->set_editable(true); + + /* Simple indent. */ + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\t"); + + /* Check input action. */ + SEND_GUI_ACTION(code_edit, "ui_text_indent"); + CHECK(code_edit->get_line(0) == "\t\t"); + + /* Insert in place. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "test\t"); + + /* Indent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->indent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Selection does entire line. */ + code_edit->set_text("test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Handles multiple lines. */ + code_edit->set_text("test\ntext"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "\ttext"); + + /* Do not indent line if last col is zero. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + + /* Indent even if last column of first line. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 4, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_selection_from_column() == 2); + CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->get_line(0) == "\ttest"); + code_edit->undo(); + } + + SUBCASE("[CodeEdit] indent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Do nothing if not editable. */ + code_edit->set_editable(false); + + code_edit->do_indent(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->indent_lines(); + CHECK(code_edit->get_line(0).is_empty()); + + code_edit->set_editable(true); + + /* Simple indent. */ + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " "); + + /* Check input action. */ + SEND_GUI_ACTION(code_edit, "ui_text_indent"); + CHECK(code_edit->get_line(0) == " "); + + /* Insert in place. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "test "); + + /* Indent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test"); + code_edit->indent_lines(); + CHECK(code_edit->get_line(0) == " test"); + + /* Selection does entire line. */ + code_edit->set_text("test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + + /* single indent only add required spaces. */ + code_edit->set_text(" test"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Handles multiple lines. */ + code_edit->set_text("test\ntext"); + code_edit->select_all(); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == " text"); + + /* Do not indent line if last col is zero. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Indent even if last column of first line. */ + code_edit->set_text("test\ntext"); + code_edit->select(0, 4, 1, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_selection_from_column() == 5); + CHECK(code_edit->get_selection_to_column() == 6); + CHECK(code_edit->get_line(0) == " test"); + } + + SUBCASE("[CodeEdit] unindent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Do nothing if not editable. */ + code_edit->set_text("\t"); + + code_edit->set_editable(false); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\t"); + + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\t"); + + code_edit->set_editable(true); + + /* Simple unindent. */ + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == ""); + + /* Should inindent inplace. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test\t"); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + + /* Backspace does a simple unindent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("\t"); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == ""); + + /* Unindent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("\ttest"); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + + /* Caret on col zero unindent line. */ + code_edit->set_text("\t\ttest"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Check input action. */ + code_edit->set_text("\t\ttest"); + SEND_GUI_ACTION(code_edit, "ui_text_dedent"); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Selection does entire line. */ + code_edit->set_text("\t\ttest"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "\ttest"); + + /* Handles multiple lines. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Do not unindent line if last col is zero. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(0, 0, 1, 0); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "\ttext"); + + /* Unindent even if last column of first line. */ + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(0, 5, 1, 1); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text("\ttest"); + code_edit->select(0, 1, 0, 2); + code_edit->do_unindent(); + CHECK(code_edit->get_selection_from_column() == 0); + CHECK(code_edit->get_selection_to_column() == 1); + CHECK(code_edit->get_line(0) == "test"); + } + + SUBCASE("[CodeEdit] unindent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Do nothing if not editable. */ + code_edit->set_text(" "); + + code_edit->set_editable(false); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " "); + + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " "); + + code_edit->set_editable(true); + + /* Simple unindent. */ + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == ""); + + /* Should inindent inplace. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test "); + + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + + /* Backspace does a simple unindent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" "); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == ""); + + /* Backspace with letter. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" a"); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == " "); + + /* Unindent lines does entire line and works without selection. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret(" test"); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + + /* Caret on col zero unindent line. */ + code_edit->set_text(" test"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Only as far as needed */ + code_edit->set_text(" test"); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Check input action. */ + code_edit->set_text(" test"); + SEND_GUI_ACTION(code_edit, "ui_text_dedent"); + CHECK(code_edit->get_line(0) == " test"); + + /* Selection does entire line. */ + code_edit->set_text(" test"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == " test"); + + /* Handles multiple lines. */ + code_edit->set_text(" test\n text"); + code_edit->select_all(); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Do not unindent line if last col is zero. */ + code_edit->set_text(" test\n text"); + code_edit->select(0, 0, 1, 0); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == " text"); + + /* Unindent even if last column of first line. */ + code_edit->set_text(" test\n text"); + code_edit->select(0, 5, 1, 1); + code_edit->do_unindent(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "text"); + + /* Check selection is adjusted. */ + code_edit->set_text(" test"); + code_edit->select(0, 4, 0, 5); + code_edit->do_unindent(); + CHECK(code_edit->get_selection_from_column() == 0); + CHECK(code_edit->get_selection_to_column() == 1); + CHECK(code_edit->get_line(0) == "test"); + } + + SUBCASE("[CodeEdit] auto indent") { + SUBCASE("[CodeEdit] auto indent tabs") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(false); + + /* Simple indent on new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == "\t"); + + /* new blank line should still indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == "\t"); + + /* new line above should not indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test:"); + + /* Whitespace between symbol and caret is okay. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: "); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: "); + CHECK(code_edit->get_line(1) == "\t"); + + /* Comment between symbol and caret is okay. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # comment"); + CHECK(code_edit->get_line(1) == "\t"); + code_edit->remove_comment_delimiter("#"); + + /* Strings between symbol and caret are not okay. */ + code_edit->add_string_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # string"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # string"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_string_delimiter("#"); + + /* Non-whitespace prevents auto-indentation. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test := 0 # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test := 0 # comment"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_comment_delimiter("#"); + + /* Even when there's no comments. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test := 0"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test := 0"); + CHECK(code_edit->get_line(1) == ""); + + /* If between brace pairs an extra line is added. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test{"); + CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_line(2) == "}"); + + /* Except when we are going above. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test{}"); + + /* or below. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test{}"); + CHECK(code_edit->get_line(1) == ""); + } + + SUBCASE("[CodeEdit] auto indent spaces") { + code_edit->set_indent_size(4); + code_edit->set_auto_indent_enabled(true); + code_edit->set_indent_using_spaces(true); + + /* Simple indent on new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == " "); + + /* new blank line should still indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test:"); + CHECK(code_edit->get_line(1) == " "); + + /* new line above should not indent. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test:"); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test:"); + + /* Whitespace between symbol and caret is okay. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: "); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: "); + CHECK(code_edit->get_line(1) == " "); + + /* Comment between symbol and caret is okay. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # comment"); + CHECK(code_edit->get_line(1) == " "); + code_edit->remove_comment_delimiter("#"); + + /* Strings between symbol and caret are not okay. */ + code_edit->add_string_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test: # string"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test: # string"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_string_delimiter("#"); + + /* Non-whitespace prevents auto-indentation. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text(""); + code_edit->insert_text_at_caret("test := 0 # comment"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test := 0 # comment"); + CHECK(code_edit->get_line(1) == ""); + code_edit->remove_comment_delimiter("#"); + + /* Even when there's no comments. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test := 0"); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test := 0"); + CHECK(code_edit->get_line(1) == ""); + + /* If between brace pairs an extra line is added. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test{"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_line(2) == "}"); + + /* Except when we are going above. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test{}"); + + /* or below. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test{}"); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test{}"); + CHECK(code_edit->get_line(1) == ""); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] folding") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + SUBCASE("[CodeEdit] folding settings") { + code_edit->set_line_folding_enabled(true); + CHECK(code_edit->is_line_folding_enabled()); + + code_edit->set_line_folding_enabled(false); + CHECK_FALSE(code_edit->is_line_folding_enabled()); + } + + SUBCASE("[CodeEdit] folding") { + code_edit->set_line_folding_enabled(true); + + // No indent. + code_edit->set_text("line1\nline2\nline3"); + for (int i = 0; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Indented lines. + code_edit->set_text("\tline1\n\tline2\n\tline3"); + for (int i = 0; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Indent. + code_edit->set_text("line1\n\tline2\nline3"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Indent with blank lines. + code_edit->set_text("line1\n\tline2\n\n\nline3"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < 2; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Nested indents. + code_edit->set_text("line1\n\tline2\n\t\tline3\nline4"); + CHECK(code_edit->can_fold_line(0)); + CHECK(code_edit->can_fold_line(1)); + for (int i = 2; i < 3; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK_FALSE(code_edit->is_line_folded(3)); + CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 2); + + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK_FALSE(code_edit->is_line_folded(3)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Check metadata. + CHECK(code_edit->get_folded_lines().size() == 1); + CHECK((int)code_edit->get_folded_lines()[0] == 0); + + // Cannot unfold nested. + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // (un)Fold all / toggle. + code_edit->unfold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Check metadata. + CHECK(code_edit->get_folded_lines().size() == 0); + + code_edit->fold_all_lines(); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + code_edit->unfold_all_lines(); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->toggle_foldable_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Can also unfold from hidden line. + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Blank lines. + code_edit->set_text("line1\n\tline2\n\n\n\ttest\n\nline3"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < code_edit->get_line_count(); i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + code_edit->fold_line(i); + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + for (int i = 1; i < code_edit->get_line_count(); i++) { + CHECK_FALSE(code_edit->is_line_folded(i)); + } + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 5); + + // End of file. + code_edit->set_text("line1\n\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Comment & string blocks. + // Single line block + code_edit->add_comment_delimiter("#", "", true); + code_edit->set_text("#line1\n#\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test #line1\n#\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("#line1\ntest #\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // String. + code_edit->add_string_delimiter("^", "", true); + code_edit->set_text("^line1\n^\tline2"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test ^line1\n^\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("^line1\ntest ^\tline2"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Multiline blocks. + code_edit->add_comment_delimiter("&", "&", false); + code_edit->set_text("&line1\n\tline2&\nline3"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Multiline comment before last line. + code_edit->set_text("&line1\nline2&\ntest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(2)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Has to be full line. + code_edit->set_text("test &line1\n\tline2&"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("&line1\n\tline2& test"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Strings. + code_edit->add_string_delimiter("$", "$", false); + code_edit->set_text("$line1\n\tline2$"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Has to be full line. + code_edit->set_text("test $line1\n\tline2$"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + code_edit->set_text("$line1\n\tline2$ test"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 1); + + // Non-indented comments/strings. + // Single line + code_edit->set_text("test\n\tline1\n#line1\n#line2\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + code_edit->set_text("test\n\tline1\n^line1\n^line2\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + // Indent level 0->1, comment after lines + code_edit->set_text("line1\n\tline2\n#test"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Indent level 0->1, comment between lines + code_edit->set_text("line1\n#test\n\tline2\nline3"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(2)); + code_edit->fold_line(2); + CHECK_FALSE(code_edit->is_line_folded(2)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Indent level 1->2, comment after lines + code_edit->set_text("\tline1\n\t\tline2\n#test"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 2); + + // Indent level 1->2, comment between lines + code_edit->set_text("\tline1\n#test\n\t\tline2\nline3"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(2)); + code_edit->fold_line(2); + CHECK_FALSE(code_edit->is_line_folded(2)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 3); + + // Multiline + code_edit->set_text("test\n\tline1\n&line1\nline2&\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + code_edit->set_text("test\n\tline1\n$line1\nline2$\n\ttest"); + CHECK(code_edit->can_fold_line(0)); + CHECK_FALSE(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK_FALSE(code_edit->is_line_folded(1)); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] completion") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + SUBCASE("[CodeEdit] auto brace completion") { + code_edit->set_auto_brace_completion_enabled(true); + CHECK(code_edit->is_auto_brace_completion_enabled()); + + code_edit->set_highlight_matching_braces_enabled(true); + CHECK(code_edit->is_highlight_matching_braces_enabled()); + + /* Try setters, any length. */ + Dictionary auto_brace_completion_pairs; + auto_brace_completion_pairs["["] = "]"; + auto_brace_completion_pairs["'"] = "'"; + auto_brace_completion_pairs[";"] = "'"; + auto_brace_completion_pairs["'''"] = "'''"; + code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + CHECK(code_edit->get_auto_brace_completion_pairs()["["] == "]"); + CHECK(code_edit->get_auto_brace_completion_pairs()["'"] == "'"); + CHECK(code_edit->get_auto_brace_completion_pairs()[";"] == "'"); + CHECK(code_edit->get_auto_brace_completion_pairs()["'''"] == "'''"); + + ERR_PRINT_OFF; + + /* No duplicate start keys. */ + code_edit->add_auto_brace_completion_pair("[", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + /* No empty keys. */ + code_edit->add_auto_brace_completion_pair("[", ""); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("", ""); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + /* Must be a symbol. */ + code_edit->add_auto_brace_completion_pair("a", "]"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("[", "a"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + code_edit->add_auto_brace_completion_pair("a", "a"); + CHECK(code_edit->get_auto_brace_completion_pairs().size() == 4); + + ERR_PRINT_ON; + + /* Check metadata. */ + CHECK(code_edit->has_auto_brace_completion_open_key("[")); + CHECK(code_edit->has_auto_brace_completion_open_key("'")); + CHECK(code_edit->has_auto_brace_completion_open_key(";")); + CHECK(code_edit->has_auto_brace_completion_open_key("'''")); + CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("(")); + + CHECK(code_edit->has_auto_brace_completion_close_key("]")); + CHECK(code_edit->has_auto_brace_completion_close_key("'")); + CHECK(code_edit->has_auto_brace_completion_close_key("'''")); + CHECK_FALSE(code_edit->has_auto_brace_completion_close_key(")")); + + CHECK(code_edit->get_auto_brace_completion_close_key("[") == "]"); + CHECK(code_edit->get_auto_brace_completion_close_key("'") == "'"); + CHECK(code_edit->get_auto_brace_completion_close_key(";") == "'"); + CHECK(code_edit->get_auto_brace_completion_close_key("'''") == "'''"); + CHECK(code_edit->get_auto_brace_completion_close_key("(").is_empty()); + + /* Check typing inserts closing pair. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT); + CHECK(code_edit->get_line(0) == "[]"); + + /* Should first match and insert smaller key. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE); + CHECK(code_edit->get_line(0) == "''"); + CHECK(code_edit->get_caret_column() == 1); + + /* Move out from centre, Should match and insert larger key. */ + SEND_GUI_ACTION(code_edit, "ui_text_caret_right"); + SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE); + CHECK(code_edit->get_line(0) == "''''''"); + CHECK(code_edit->get_caret_column() == 3); + + /* Backspace should remove all. */ + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->get_line(0).is_empty()); + + /* If in between and typing close key should "skip". */ + SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT); + CHECK(code_edit->get_line(0) == "[]"); + CHECK(code_edit->get_caret_column() == 1); + SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETRIGHT); + CHECK(code_edit->get_line(0) == "[]"); + CHECK(code_edit->get_caret_column() == 2); + + /* If current is char and inserting a string, do not autocomplete. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, Key::A); + SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE); + CHECK(code_edit->get_line(0) == "A'"); + + /* If in comment, do not complete. */ + code_edit->add_comment_delimiter("#", ""); + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, Key::NUMBERSIGN); + SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE); + CHECK(code_edit->get_line(0) == "#'"); + + /* If in string, and inserting string do not complete. */ + code_edit->clear(); + SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE); + SEND_GUI_KEY_EVENT(code_edit, Key::QUOTEDBL); + CHECK(code_edit->get_line(0) == "'\"'"); + + /* Wrap single line selection with brackets */ + code_edit->clear(); + code_edit->insert_text_at_caret("abc"); + code_edit->select_all(); + SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT); + CHECK(code_edit->get_line(0) == "[abc]"); + + /* Caret should be after the last character of the single line selection */ + CHECK(code_edit->get_caret_column() == 4); + + /* Wrap multi line selection with brackets */ + code_edit->clear(); + code_edit->insert_text_at_caret("abc\nabc"); + code_edit->select_all(); + SEND_GUI_KEY_EVENT(code_edit, Key::BRACKETLEFT); + CHECK(code_edit->get_text() == "[abc\nabc]"); + + /* Caret should be after the last character of the multi line selection */ + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 3); + + /* If inserted character is not a auto brace completion open key, replace selected text with the inserted character */ + code_edit->clear(); + code_edit->insert_text_at_caret("abc"); + code_edit->select_all(); + SEND_GUI_KEY_EVENT(code_edit, Key::KEY_1); + CHECK(code_edit->get_text() == "1"); + + /* If potential multichar and single brace completion is matched, it should wrap the single. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\'\'abc"); + code_edit->select(0, 2, 0, 5); + SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE); + CHECK(code_edit->get_text() == "\'\'\'abc\'"); + + /* If only the potential multichar brace completion is matched, it does not wrap or complete. */ + auto_brace_completion_pairs.erase("\'"); + code_edit->set_auto_brace_completion_pairs(auto_brace_completion_pairs); + CHECK_FALSE(code_edit->has_auto_brace_completion_open_key("\'")); + + code_edit->clear(); + code_edit->insert_text_at_caret("\'\'abc"); + code_edit->select(0, 2, 0, 5); + SEND_GUI_KEY_EVENT(code_edit, Key::APOSTROPHE); + CHECK(code_edit->get_text() == "\'\'\'"); + } + + SUBCASE("[CodeEdit] autocomplete") { + code_edit->set_code_completion_enabled(true); + CHECK(code_edit->is_code_completion_enabled()); + + /* Set prefixes, single char only, disallow empty. */ + TypedArray<String> completion_prefixes; + completion_prefixes.push_back(""); + completion_prefixes.push_back("."); + completion_prefixes.push_back("."); + completion_prefixes.push_back(",,"); + + ERR_PRINT_OFF; + code_edit->set_code_completion_prefixes(completion_prefixes); + ERR_PRINT_ON; + completion_prefixes = code_edit->get_code_completion_prefixes(); + CHECK(completion_prefixes.size() == 2); + CHECK(completion_prefixes.has(".")); + CHECK(completion_prefixes.has(",")); + + code_edit->set_text("test\ntest"); + CHECK(code_edit->get_text_for_code_completion() == String::chr(0xFFFF) + "test\ntest"); + } + + SUBCASE("[CodeEdit] autocomplete request") { + SIGNAL_WATCH(code_edit, "code_completion_requested"); + code_edit->set_code_completion_enabled(true); + + Array signal_args; + signal_args.push_back(Array()); + + /* Force request. */ + code_edit->request_code_completion(); + SIGNAL_CHECK_FALSE("code_completion_requested"); + code_edit->request_code_completion(true); + SIGNAL_CHECK("code_completion_requested", signal_args); + + /* Manual request should force. */ + SEND_GUI_ACTION(code_edit, "ui_text_completion_query"); + SIGNAL_CHECK("code_completion_requested", signal_args); + + /* Insert prefix. */ + TypedArray<String> completion_prefixes; + completion_prefixes.push_back("."); + code_edit->set_code_completion_prefixes(completion_prefixes); + + code_edit->insert_text_at_caret("."); + code_edit->request_code_completion(); + SIGNAL_CHECK("code_completion_requested", signal_args); + + /* Should work with space too. */ + code_edit->insert_text_at_caret(" "); + code_edit->request_code_completion(); + SIGNAL_CHECK("code_completion_requested", signal_args); + + /* Should work when complete ends with prefix. */ + code_edit->clear(); + code_edit->insert_text_at_caret("t"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "test.", "test."); + code_edit->update_code_completion_options(); + code_edit->confirm_code_completion(); + CHECK(code_edit->get_line(0) == "test."); + SIGNAL_CHECK("code_completion_requested", signal_args); + + SIGNAL_UNWATCH(code_edit, "code_completion_requested"); + } + + SUBCASE("[CodeEdit] autocomplete completion") { + if (TS->has_feature(TextServer::FEATURE_FONT_DYNAMIC) && TS->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) { + CHECK(code_edit->get_code_completion_selected_index() == -1); + code_edit->set_code_completion_enabled(true); + CHECK(code_edit->get_code_completion_selected_index() == -1); + + code_edit->update_code_completion_options(); + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_code_completion_option(0).size() == 0); + CHECK(code_edit->get_code_completion_options().size() == 0); + + /* Adding does not update the list. */ + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_0.", "item_0"); + + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_code_completion_option(0).size() == 0); + CHECK(code_edit->get_code_completion_options().size() == 0); + + /* After update, pending add should not be counted, */ + /* also does not work on col 0 */ + code_edit->insert_text_at_caret("i"); + code_edit->update_code_completion_options(); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0", Color(1, 0, 0), Ref<Resource>(), Color(1, 0, 0)); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_1.", "item_1"); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_VARIABLE, "item_2.", "item_2"); + + ERR_PRINT_OFF; + code_edit->set_code_completion_selected_index(1); + ERR_PRINT_ON; + CHECK(code_edit->get_code_completion_selected_index() == 0); + CHECK(code_edit->get_code_completion_option(0).size() == 6); + CHECK(code_edit->get_code_completion_options().size() == 1); + + /* Check cancel closes completion. */ + SEND_GUI_ACTION(code_edit, "ui_cancel"); + CHECK(code_edit->get_code_completion_selected_index() == -1); + + code_edit->update_code_completion_options(); + CHECK(code_edit->get_code_completion_selected_index() == 0); + code_edit->set_code_completion_selected_index(1); + CHECK(code_edit->get_code_completion_selected_index() == 1); + CHECK(code_edit->get_code_completion_option(0).size() == 6); + CHECK(code_edit->get_code_completion_options().size() == 3); + + /* Check data. */ + Dictionary option = code_edit->get_code_completion_option(0); + CHECK((int)option["kind"] == (int)CodeEdit::CodeCompletionKind::KIND_CLASS); + CHECK(option["display_text"] == "item_0."); + CHECK(option["insert_text"] == "item_0"); + CHECK(option["font_color"] == Color(1, 0, 0)); + CHECK(option["icon"] == Ref<Resource>()); + CHECK(option["default_value"] == Color(1, 0, 0)); + + /* Set size for mouse input. */ + code_edit->set_size(Size2(100, 100)); + + /* Check input. */ + SEND_GUI_ACTION(code_edit, "ui_end"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_home"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_page_down"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_page_up"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_up"); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + SEND_GUI_ACTION(code_edit, "ui_down"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_KEY_EVENT(code_edit, Key::T); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_left"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_right"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + SEND_GUI_ACTION(code_edit, "ui_text_backspace"); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + Point2 caret_pos = code_edit->get_caret_draw_pos(); + caret_pos.y -= code_edit->get_line_height(); + SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::WHEEL_DOWN, MouseButton::NONE, Key::NONE); + CHECK(code_edit->get_code_completion_selected_index() == 1); + + SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::WHEEL_UP, MouseButton::NONE, Key::NONE); + CHECK(code_edit->get_code_completion_selected_index() == 0); + + /* Single click selects. */ + SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK(code_edit->get_code_completion_selected_index() == 2); + + /* Double click inserts. */ + SEND_GUI_DOUBLE_CLICK(code_edit, caret_pos, Key::NONE); + CHECK(code_edit->get_code_completion_selected_index() == -1); + CHECK(code_edit->get_line(0) == "item_2"); + + code_edit->set_auto_brace_completion_enabled(false); + + /* Does nothing in readonly. */ + code_edit->undo(); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + code_edit->set_editable(false); + code_edit->confirm_code_completion(); + code_edit->set_editable(true); + CHECK(code_edit->get_line(0) == "i"); + + /* Replace */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0 test"); + + /* Replace string. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test\""); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "\"item_0\""); + + /* Normal replace if no end is given. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "\"item_0\" test"); + + /* Insert at completion. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_accept"); + CHECK(code_edit->get_line(0) == "item_01 test"); + + /* Insert at completion with string should have same output. */ + code_edit->clear(); + code_edit->insert_text_at_caret("\"item_1 test\""); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0.", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_accept"); + CHECK(code_edit->get_line(0) == "\"item_0\"1 test\""); + + /* Merge symbol at end on insert text. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* Full set. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* Autobrace completion. */ + code_edit->set_auto_brace_completion_enabled(true); + + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 7); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1( test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0(", "item_0("); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0( test"); + CHECK(code_edit->get_caret_column() == 7); + + /* Full set. */ + /* End on completion entry. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1 test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + + /* End of text*/ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0", "item_0"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 6); + + /* End of both. */ + code_edit->clear(); + code_edit->insert_text_at_caret("item_1() test"); + code_edit->set_caret_column(2); + code_edit->add_code_completion_option(CodeEdit::CodeCompletionKind::KIND_CLASS, "item_0()", "item_0()"); + code_edit->update_code_completion_options(); + SEND_GUI_ACTION(code_edit, "ui_text_completion_replace"); + CHECK(code_edit->get_line(0) == "item_0() test"); + CHECK(code_edit->get_caret_column() == 8); + } + } + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] symbol lookup") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + code_edit->set_symbol_lookup_on_click_enabled(true); + CHECK(code_edit->is_symbol_lookup_on_click_enabled()); + + if (TS->has_feature(TextServer::FEATURE_FONT_DYNAMIC) && TS->has_feature(TextServer::FEATURE_SIMPLE_LAYOUT)) { + /* Set size for mouse input. */ + code_edit->set_size(Size2(100, 100)); + + code_edit->set_text("this is some text"); + + Point2 caret_pos = code_edit->get_caret_draw_pos(); + caret_pos.x += 60; + SEND_GUI_MOUSE_BUTTON_EVENT(code_edit, caret_pos, MouseButton::NONE, MouseButton::NONE, Key::NONE); + CHECK(code_edit->get_text_for_symbol_lookup() == "this is s" + String::chr(0xFFFF) + "ome text"); + + SIGNAL_WATCH(code_edit, "symbol_validate"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(code_edit, Key::META); +#else + SEND_GUI_KEY_EVENT(code_edit, Key::CTRL); +#endif + + Array signal_args; + Array arg; + arg.push_back("some"); + signal_args.push_back(arg); + SIGNAL_CHECK("symbol_validate", signal_args); + + SIGNAL_UNWATCH(code_edit, "symbol_validate"); + + memdelete(code_edit); + } +} + +TEST_CASE("[SceneTree][CodeEdit] line length guidelines") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + TypedArray<int> guide_lines; + + code_edit->set_line_length_guidelines(guide_lines); + CHECK(code_edit->get_line_length_guidelines().size() == 0); + + guide_lines.push_back(80); + guide_lines.push_back(120); + + /* Order should be preserved. */ + code_edit->set_line_length_guidelines(guide_lines); + CHECK((int)code_edit->get_line_length_guidelines()[0] == 80); + CHECK((int)code_edit->get_line_length_guidelines()[1] == 120); + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] Backspace delete") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + /* Backspace with selection on first line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test backspace"); + code_edit->select(0, 0, 0, 5); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + + /* Backspace with selection on first line and caret at the beginning of file. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test backspace"); + code_edit->select(0, 0, 0, 5); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + + /* Move caret up to the previous line on backspace if caret is at the first column. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "line 1line 2"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); + + /* Backspace delete all text if all text is selected. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); + code_edit->select_all(); + code_edit->backspace(); + CHECK(code_edit->get_text().is_empty()); + + /* Backspace at the beginning without selection has no effect. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1\nline 2\nline 3"); + + memdelete(code_edit); +} + +TEST_CASE("[SceneTree][CodeEdit] New Line") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + /* Add a new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(13); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + + /* Split line with new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == "test "); + CHECK(code_edit->get_line(1) == "new line"); + + /* Delete selection and split with new line. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION(code_edit, "ui_text_newline"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "new line"); + + /* Blank new line below with selection should not split. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + + /* Blank new line above with selection should not split. */ + code_edit->set_text(""); + code_edit->insert_text_at_caret("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION(code_edit, "ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test new line"); + + memdelete(code_edit); +} + +} // namespace TestCodeEdit + +#endif // TEST_CODE_EDIT_H diff --git a/tests/scene/test_curve.h b/tests/scene/test_curve.h new file mode 100644 index 0000000000..ad7625ddc5 --- /dev/null +++ b/tests/scene/test_curve.h @@ -0,0 +1,254 @@ +/*************************************************************************/ +/* test_curve.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_CURVE_H +#define TEST_CURVE_H + +#include "scene/resources/curve.h" + +#include "tests/test_macros.h" + +namespace TestCurve { + +TEST_CASE("[Curve] Default curve") { + const Ref<Curve> curve = memnew(Curve); + + CHECK_MESSAGE( + curve->get_point_count() == 0, + "Default curve should contain the expected number of points."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->sample(0)), + "Default curve should return the expected value at offset 0.0."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->sample(0.5)), + "Default curve should return the expected value at offset 0.5."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->sample(1)), + "Default curve should return the expected value at offset 1.0."); +} + +TEST_CASE("[Curve] Custom curve with free tangents") { + Ref<Curve> curve = memnew(Curve); + // "Sawtooth" curve with an open ending towards the 1.0 offset. + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(0.25, 1)); + curve->add_point(Vector2(0.5, 0)); + curve->add_point(Vector2(0.75, 1)); + + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_left_tangent(0)), + "get_point_left_tangent() should return the expected value for point index 0."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_right_tangent(0)), + "get_point_right_tangent() should return the expected value for point index 0."); + CHECK_MESSAGE( + curve->get_point_left_mode(0) == Curve::TangentMode::TANGENT_FREE, + "get_point_left_mode() should return the expected value for point index 0."); + CHECK_MESSAGE( + curve->get_point_right_mode(0) == Curve::TangentMode::TANGENT_FREE, + "get_point_right_mode() should return the expected value for point index 0."); + + CHECK_MESSAGE( + curve->get_point_count() == 4, + "Custom free curve should contain the expected number of points."); + + CHECK_MESSAGE( + Math::is_zero_approx(curve->sample(-0.1)), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.1), (real_t)0.352), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.4), (real_t)0.352), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.7), (real_t)0.896), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(1), 1), + "Custom free curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(2), 1), + "Custom free curve should return the expected value at offset 0.1."); + + CHECK_MESSAGE( + Math::is_zero_approx(curve->sample_baked(-0.1)), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.1), (real_t)0.352), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.4), (real_t)0.352), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.7), (real_t)0.896), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(1), 1), + "Custom free curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(2), 1), + "Custom free curve should return the expected baked value at offset 0.1."); + + curve->remove_point(1); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.1), 0), + "Custom free curve should return the expected value at offset 0.1 after removing point at index 1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.1), 0), + "Custom free curve should return the expected baked value at offset 0.1 after removing point at index 1."); + + curve->clear_points(); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.6), 0), + "Custom free curve should return the expected value at offset 0.6 after clearing all points."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.6), 0), + "Custom free curve should return the expected baked value at offset 0.6 after clearing all points."); +} + +TEST_CASE("[Curve] Custom curve with linear tangents") { + Ref<Curve> curve = memnew(Curve); + // "Sawtooth" curve with an open ending towards the 1.0 offset. + curve->add_point(Vector2(0, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + curve->add_point(Vector2(0.25, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + curve->add_point(Vector2(0.5, 0), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + curve->add_point(Vector2(0.75, 1), 0, 0, Curve::TangentMode::TANGENT_LINEAR, Curve::TangentMode::TANGENT_LINEAR); + + CHECK_MESSAGE( + Math::is_equal_approx(curve->get_point_left_tangent(3), 4), + "get_point_left_tangent() should return the expected value for point index 3."); + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_right_tangent(3)), + "get_point_right_tangent() should return the expected value for point index 3."); + CHECK_MESSAGE( + curve->get_point_left_mode(3) == Curve::TangentMode::TANGENT_LINEAR, + "get_point_left_mode() should return the expected value for point index 3."); + CHECK_MESSAGE( + curve->get_point_right_mode(3) == Curve::TangentMode::TANGENT_LINEAR, + "get_point_right_mode() should return the expected value for point index 3."); + + ERR_PRINT_OFF; + CHECK_MESSAGE( + Math::is_zero_approx(curve->get_point_right_tangent(300)), + "get_point_right_tangent() should return the expected value for invalid point index 300."); + CHECK_MESSAGE( + curve->get_point_left_mode(-12345) == Curve::TangentMode::TANGENT_FREE, + "get_point_left_mode() should return the expected value for invalid point index -12345."); + ERR_PRINT_ON; + + CHECK_MESSAGE( + curve->get_point_count() == 4, + "Custom linear curve should contain the expected number of points."); + + CHECK_MESSAGE( + Math::is_zero_approx(curve->sample(-0.1)), + "Custom linear curve should return the expected value at offset -0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.1), (real_t)0.4), + "Custom linear curve should return the expected value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.4), (real_t)0.4), + "Custom linear curve should return the expected value at offset 0.4."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.7), (real_t)0.8), + "Custom linear curve should return the expected value at offset 0.7."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(1), 1), + "Custom linear curve should return the expected value at offset 1.0."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(2), 1), + "Custom linear curve should return the expected value at offset 2.0."); + + CHECK_MESSAGE( + Math::is_zero_approx(curve->sample_baked(-0.1)), + "Custom linear curve should return the expected baked value at offset -0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.1), (real_t)0.4), + "Custom linear curve should return the expected baked value at offset 0.1."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.4), (real_t)0.4), + "Custom linear curve should return the expected baked value at offset 0.4."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.7), (real_t)0.8), + "Custom linear curve should return the expected baked value at offset 0.7."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(1), 1), + "Custom linear curve should return the expected baked value at offset 1.0."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(2), 1), + "Custom linear curve should return the expected baked value at offset 2.0."); + + ERR_PRINT_OFF; + curve->remove_point(10); + ERR_PRINT_ON; + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample(0.7), (real_t)0.8), + "Custom free curve should return the expected value at offset 0.7 after removing point at invalid index 10."); + CHECK_MESSAGE( + Math::is_equal_approx(curve->sample_baked(0.7), (real_t)0.8), + "Custom free curve should return the expected baked value at offset 0.7 after removing point at invalid index 10."); +} + +TEST_CASE("[Curve2D] Linear sampling should return exact value") { + Ref<Curve2D> curve = memnew(Curve2D); + real_t len = 2048.0; + + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(len, 0)); + + real_t baked_length = curve->get_baked_length(); + CHECK(len == baked_length); + + for (int i = 0; i < len; i++) { + Vector2 pos = curve->sample_baked(i); + CHECK_MESSAGE(pos.x == i, "sample_baked should return exact value"); + } +} + +TEST_CASE("[Curve3D] Linear sampling should return exact value") { + Ref<Curve3D> curve = memnew(Curve3D); + real_t len = 2048.0; + + curve->add_point(Vector3(0, 0, 0)); + curve->add_point(Vector3(len, 0, 0)); + + real_t baked_length = curve->get_baked_length(); + CHECK(len == baked_length); + + for (int i = 0; i < len; i++) { + Vector3 pos = curve->sample_baked(i); + CHECK_MESSAGE(pos.x == i, "sample_baked should return exact value"); + } +} + +} // namespace TestCurve + +#endif // TEST_CURVE_H diff --git a/tests/scene/test_gradient.h b/tests/scene/test_gradient.h new file mode 100644 index 0000000000..b0e6128932 --- /dev/null +++ b/tests/scene/test_gradient.h @@ -0,0 +1,149 @@ +/*************************************************************************/ +/* test_gradient.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_GRADIENT_H +#define TEST_GRADIENT_H + +#include "scene/resources/gradient.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestGradient { + +TEST_CASE("[Gradient] Default gradient") { + // Black-white gradient. + Ref<Gradient> gradient = memnew(Gradient); + + CHECK_MESSAGE( + gradient->get_points_count() == 2, + "Default gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(0, 0, 0)), + "Default gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.4).is_equal_approx(Color(0.4, 0.4, 0.4)), + "Default gradient should return the expected interpolated value at offset 0.4."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.8).is_equal_approx(Color(0.8, 0.8, 0.8)), + "Default gradient should return the expected interpolated value at offset 0.8."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(1, 1, 1)), + "Default gradient should return the expected interpolated value at offset 1.0."); + + // Out of bounds checks. + CHECK_MESSAGE( + gradient->get_color_at_offset(-1.0).is_equal_approx(Color(0, 0, 0)), + "Default gradient should return the expected interpolated value at offset -1.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1234.0).is_equal_approx(Color(1, 1, 1)), + "Default gradient should return the expected interpolated value at offset 1234.0."); +} + +TEST_CASE("[Gradient] Custom gradient (points specified in order)") { + // Red-yellow-green gradient (with overbright green). + Ref<Gradient> gradient = memnew(Gradient); + Vector<Gradient::Point> points; + points.push_back({ 0.0, Color(1, 0, 0) }); + points.push_back({ 0.5, Color(1, 1, 0) }); + points.push_back({ 1.0, Color(0, 2, 0) }); + gradient->set_points(points); + + CHECK_MESSAGE( + gradient->get_points_count() == 3, + "Custom gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 0, 0)), + "Custom gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.25).is_equal_approx(Color(1, 0.5, 0)), + "Custom gradient should return the expected interpolated value at offset 0.25."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.5).is_equal_approx(Color(1, 1, 0)), + "Custom gradient should return the expected interpolated value at offset 0.5."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.75).is_equal_approx(Color(0.5, 1.5, 0)), + "Custom gradient should return the expected interpolated value at offset 0.75."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 2, 0)), + "Custom gradient should return the expected interpolated value at offset 1.0."); + + gradient->remove_point(1); + CHECK_MESSAGE( + gradient->get_points_count() == 2, + "Custom gradient should contain the expected number of points after removing one point."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.5).is_equal_approx(Color(0.5, 1, 0)), + "Custom gradient should return the expected interpolated value at offset 0.5 after removing point at index 1."); +} + +TEST_CASE("[Gradient] Custom gradient (points specified out-of-order)") { + // HSL rainbow with points specified out of order. + // These should be sorted automatically when adding points. + Ref<Gradient> gradient = memnew(Gradient); + Vector<Gradient::Point> points; + points.push_back({ 0.2, Color(1, 0, 0) }); + points.push_back({ 0.0, Color(1, 1, 0) }); + points.push_back({ 0.8, Color(0, 1, 0) }); + points.push_back({ 0.4, Color(0, 1, 1) }); + points.push_back({ 1.0, Color(0, 0, 1) }); + points.push_back({ 0.6, Color(1, 0, 1) }); + gradient->set_points(points); + + CHECK_MESSAGE( + gradient->get_points_count() == 6, + "Custom out-of-order gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 1, 0)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.3).is_equal_approx(Color(0.5, 0.5, 0.5)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.3."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.6).is_equal_approx(Color(1, 0, 1)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.6."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 0, 1)), + "Custom out-of-order gradient should return the expected interpolated value at offset 1.0."); + + gradient->remove_point(0); + CHECK_MESSAGE( + gradient->get_points_count() == 5, + "Custom out-of-order gradient should contain the expected number of points after removing one point."); + // The color will be clamped to the nearest point (which is at offset 0.2). + CHECK_MESSAGE( + gradient->get_color_at_offset(0.1).is_equal_approx(Color(1, 0, 0)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.1 after removing point at index 0."); +} +} // namespace TestGradient + +#endif // TEST_GRADIENT_H diff --git a/tests/scene/test_path_3d.h b/tests/scene/test_path_3d.h new file mode 100644 index 0000000000..8ac3d7b5b4 --- /dev/null +++ b/tests/scene/test_path_3d.h @@ -0,0 +1,84 @@ +/*************************************************************************/ +/* test_path_3d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_PATH_3D_H +#define TEST_PATH_3D_H + +#include "scene/3d/path_3d.h" + +#include "tests/test_macros.h" + +namespace TestPath3D { + +TEST_CASE("[Path3D] Initialization") { + SUBCASE("Path should be empty right after initialization") { + Path3D *test_path = memnew(Path3D); + CHECK(test_path->get_curve() == nullptr); + memdelete(test_path); + } +} + +TEST_CASE("[Path3D] Curve setter and getter") { + SUBCASE("Curve passed to the class should remain the same") { + Path3D *test_path = memnew(Path3D); + const Ref<Curve3D> &curve = memnew(Curve3D); + + test_path->set_curve(curve); + CHECK(test_path->get_curve() == curve); + memdelete(test_path); + } + SUBCASE("Curve passed many times to the class should remain the same") { + Path3D *test_path = memnew(Path3D); + const Ref<Curve3D> &curve = memnew(Curve3D); + + test_path->set_curve(curve); + test_path->set_curve(curve); + test_path->set_curve(curve); + CHECK(test_path->get_curve() == curve); + memdelete(test_path); + } + SUBCASE("Curve rewrite testing") { + Path3D *test_path = memnew(Path3D); + const Ref<Curve3D> &curve1 = memnew(Curve3D); + const Ref<Curve3D> &curve2 = memnew(Curve3D); + + test_path->set_curve(curve1); + test_path->set_curve(curve2); + CHECK_MESSAGE(test_path->get_curve() != curve1, + "After rewrite, second curve should be in class"); + CHECK_MESSAGE(test_path->get_curve() == curve2, + "After rewrite, second curve should be in class"); + memdelete(test_path); + } +} + +} // namespace TestPath3D + +#endif // TEST_PATH_3D_H diff --git a/tests/scene/test_path_follow_2d.h b/tests/scene/test_path_follow_2d.h new file mode 100644 index 0000000000..57261116a2 --- /dev/null +++ b/tests/scene/test_path_follow_2d.h @@ -0,0 +1,240 @@ +/*************************************************************************/ +/* test_path_follow_2d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_PATH_FOLLOW_2D_H +#define TEST_PATH_FOLLOW_2D_H + +#include "scene/2d/path_2d.h" + +#include "tests/test_macros.h" + +namespace TestPathFollow2D { + +TEST_CASE("[PathFollow2D] Sampling with progress ratio") { + const Ref<Curve2D> &curve = memnew(Curve2D()); + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(100, 0)); + curve->add_point(Vector2(100, 100)); + curve->add_point(Vector2(0, 100)); + curve->add_point(Vector2(0, 0)); + const Path2D *path = memnew(Path2D); + path->set_curve(curve); + const PathFollow2D *path_follow_2d = memnew(PathFollow2D); + path->add_child(path_follow_2d); + + path_follow_2d->set_progress_ratio(0); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0))); + + path_follow_2d->set_progress_ratio(0.125); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0))); + + path_follow_2d->set_progress_ratio(0.25); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0))); + + path_follow_2d->set_progress_ratio(0.375); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 50))); + + path_follow_2d->set_progress_ratio(0.5); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 100))); + + path_follow_2d->set_progress_ratio(0.625); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 100))); + + path_follow_2d->set_progress_ratio(0.75); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 100))); + + path_follow_2d->set_progress_ratio(0.875); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 50))); + + path_follow_2d->set_progress_ratio(1); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0))); + + memdelete(path); +} + +TEST_CASE("[PathFollow2D] Sampling with progress") { + const Ref<Curve2D> &curve = memnew(Curve2D()); + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(100, 0)); + curve->add_point(Vector2(100, 100)); + curve->add_point(Vector2(0, 100)); + curve->add_point(Vector2(0, 0)); + const Path2D *path = memnew(Path2D); + path->set_curve(curve); + const PathFollow2D *path_follow_2d = memnew(PathFollow2D); + path->add_child(path_follow_2d); + + path_follow_2d->set_progress(0); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0))); + + path_follow_2d->set_progress(50); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0))); + + path_follow_2d->set_progress(100); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0))); + + path_follow_2d->set_progress(150); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 50))); + + path_follow_2d->set_progress(200); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 100))); + + path_follow_2d->set_progress(250); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 100))); + + path_follow_2d->set_progress(300); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 100))); + + path_follow_2d->set_progress(350); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 50))); + + path_follow_2d->set_progress(400); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(0, 0))); + + memdelete(path); +} + +TEST_CASE("[PathFollow2D] Removal of a point in curve") { + const Ref<Curve2D> &curve = memnew(Curve2D()); + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(100, 0)); + curve->add_point(Vector2(100, 100)); + const Path2D *path = memnew(Path2D); + path->set_curve(curve); + const PathFollow2D *path_follow_2d = memnew(PathFollow2D); + path->add_child(path_follow_2d); + + path_follow_2d->set_progress_ratio(0.5); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(100, 0))); + + curve->remove_point(1); + + CHECK_MESSAGE( + path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 50)), + "Path follow's position should be updated after removing a point from the curve"); + + memdelete(path); +} + +TEST_CASE("[PathFollow2D] Setting h_offset and v_offset") { + const Ref<Curve2D> &curve = memnew(Curve2D()); + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(100, 0)); + const Path2D *path = memnew(Path2D); + path->set_curve(curve); + const PathFollow2D *path_follow_2d = memnew(PathFollow2D); + path->add_child(path_follow_2d); + + path_follow_2d->set_progress_ratio(0.5); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(50, 0))); + + path_follow_2d->set_h_offset(25); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(75, 0))); + + path_follow_2d->set_v_offset(25); + CHECK(path_follow_2d->get_transform().get_origin().is_equal_approx(Vector2(75, 25))); + + memdelete(path); +} + +TEST_CASE("[PathFollow2D] Unit offset out of range") { + const Ref<Curve2D> &curve = memnew(Curve2D()); + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(100, 0)); + const Path2D *path = memnew(Path2D); + path->set_curve(curve); + const PathFollow2D *path_follow_2d = memnew(PathFollow2D); + path->add_child(path_follow_2d); + + path_follow_2d->set_loop(true); + + path_follow_2d->set_progress_ratio(-0.3); + CHECK_MESSAGE( + path_follow_2d->get_progress_ratio() == 0.7, + "Progress Ratio should loop back from the end in the opposite direction"); + + path_follow_2d->set_progress_ratio(1.3); + CHECK_MESSAGE( + path_follow_2d->get_progress_ratio() == 0.3, + "Progress Ratio should loop back from the end in the opposite direction"); + + path_follow_2d->set_loop(false); + + path_follow_2d->set_progress_ratio(-0.3); + CHECK_MESSAGE( + path_follow_2d->get_progress_ratio() == 0, + "Progress Ratio should be clamped at 0"); + + path_follow_2d->set_progress_ratio(1.3); + CHECK_MESSAGE( + path_follow_2d->get_progress_ratio() == 1, + "Progress Ratio should be clamped at 1"); + + memdelete(path); +} + +TEST_CASE("[PathFollow2D] Progress out of range") { + const Ref<Curve2D> &curve = memnew(Curve2D()); + curve->add_point(Vector2(0, 0)); + curve->add_point(Vector2(100, 0)); + const Path2D *path = memnew(Path2D); + path->set_curve(curve); + const PathFollow2D *path_follow_2d = memnew(PathFollow2D); + path->add_child(path_follow_2d); + + path_follow_2d->set_loop(true); + + path_follow_2d->set_progress(-50); + CHECK_MESSAGE( + path_follow_2d->get_progress() == 50, + "Progress should loop back from the end in the opposite direction"); + + path_follow_2d->set_progress(150); + CHECK_MESSAGE( + path_follow_2d->get_progress() == 50, + "Progress should loop back from the end in the opposite direction"); + + path_follow_2d->set_loop(false); + + path_follow_2d->set_progress(-50); + CHECK_MESSAGE( + path_follow_2d->get_progress() == 0, + "Progress should be clamped at 0"); + + path_follow_2d->set_progress(150); + CHECK_MESSAGE( + path_follow_2d->get_progress() == 100, + "Progress should be clamped at 1"); + + memdelete(path); +} +} // namespace TestPathFollow2D + +#endif // TEST_PATH_FOLLOW_2D_H diff --git a/tests/scene/test_path_follow_3d.h b/tests/scene/test_path_follow_3d.h new file mode 100644 index 0000000000..6334fa56de --- /dev/null +++ b/tests/scene/test_path_follow_3d.h @@ -0,0 +1,219 @@ +/*************************************************************************/ +/* test_path_follow_3d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_PATH_FOLLOW_3D_H +#define TEST_PATH_FOLLOW_3D_H + +#include "scene/3d/path_3d.h" + +#include "tests/test_macros.h" + +namespace TestPathFollow3D { + +TEST_CASE("[PathFollow3D] Sampling with progress ratio") { + const Ref<Curve3D> &curve = memnew(Curve3D()); + curve->add_point(Vector3(0, 0, 0)); + curve->add_point(Vector3(100, 0, 0)); + curve->add_point(Vector3(100, 100, 0)); + curve->add_point(Vector3(100, 100, 100)); + curve->add_point(Vector3(100, 0, 100)); + const Path3D *path = memnew(Path3D); + path->set_curve(curve); + const PathFollow3D *path_follow_3d = memnew(PathFollow3D); + path->add_child(path_follow_3d); + + path_follow_3d->set_progress_ratio(0); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(0, 0, 0)); + + path_follow_3d->set_progress_ratio(0.125); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(50, 0, 0)); + + path_follow_3d->set_progress_ratio(0.25); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 0); + + path_follow_3d->set_progress_ratio(0.375); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 0))); + + path_follow_3d->set_progress_ratio(0.5); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 0))); + + path_follow_3d->set_progress_ratio(0.625); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 50))); + + path_follow_3d->set_progress_ratio(0.75); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 100))); + + path_follow_3d->set_progress_ratio(0.875); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 100))); + + path_follow_3d->set_progress_ratio(1); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 100))); + + memdelete(path); +} + +TEST_CASE("[PathFollow3D] Sampling with progress") { + const Ref<Curve3D> &curve = memnew(Curve3D()); + curve->add_point(Vector3(0, 0, 0)); + curve->add_point(Vector3(100, 0, 0)); + curve->add_point(Vector3(100, 100, 0)); + curve->add_point(Vector3(100, 100, 100)); + curve->add_point(Vector3(100, 0, 100)); + const Path3D *path = memnew(Path3D); + path->set_curve(curve); + const PathFollow3D *path_follow_3d = memnew(PathFollow3D); + path->add_child(path_follow_3d); + + path_follow_3d->set_progress(0); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(0, 0, 0)); + + path_follow_3d->set_progress(50); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(50, 0, 0)); + + path_follow_3d->set_progress(100); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 0); + + path_follow_3d->set_progress(150); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 0))); + + path_follow_3d->set_progress(200); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 0))); + + path_follow_3d->set_progress(250); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 50))); + + path_follow_3d->set_progress(300); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 100, 100))); + + path_follow_3d->set_progress(350); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 50, 100))); + + path_follow_3d->set_progress(400); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector3(100, 0, 100))); + + memdelete(path); +} + +TEST_CASE("[PathFollow3D] Removal of a point in curve") { + const Ref<Curve3D> &curve = memnew(Curve3D()); + curve->add_point(Vector3(0, 0, 0)); + curve->add_point(Vector3(100, 0, 0)); + curve->add_point(Vector3(100, 100, 0)); + const Path3D *path = memnew(Path3D); + path->set_curve(curve); + const PathFollow3D *path_follow_3d = memnew(PathFollow3D); + path->add_child(path_follow_3d); + + path_follow_3d->set_progress_ratio(0.5); + CHECK(path_follow_3d->get_transform().get_origin().is_equal_approx(Vector2(100, 0, 0))); + + curve->remove_point(1); + + CHECK_MESSAGE( + path_follow_3d->get_transform().get_origin().is_equal_approx(Vector2(50, 50, 0)), + "Path follow's position should be updated after removing a point from the curve"); + + memdelete(path); +} + +TEST_CASE("[PathFollow3D] Progress ratio out of range") { + const Ref<Curve3D> &curve = memnew(Curve3D()); + curve->add_point(Vector3(0, 0, 0)); + curve->add_point(Vector3(100, 0, 0)); + const Path3D *path = memnew(Path3D); + path->set_curve(curve); + const PathFollow3D *path_follow_3d = memnew(PathFollow3D); + path->add_child(path_follow_3d); + + path_follow_3d->set_loop(true); + + path_follow_3d->set_progress_ratio(-0.3); + CHECK_MESSAGE( + path_follow_3d->get_progress_ratio() == 0.7, + "Progress Ratio should loop back from the end in the opposite direction"); + + path_follow_3d->set_progress_ratio(1.3); + CHECK_MESSAGE( + path_follow_3d->get_progress_ratio() == 0.3, + "Progress Ratio should loop back from the end in the opposite direction"); + + path_follow_3d->set_loop(false); + + path_follow_3d->set_progress_ratio(-0.3); + CHECK_MESSAGE( + path_follow_3d->get_progress_ratio() == 0, + "Progress Ratio should be clamped at 0"); + + path_follow_3d->set_progress_ratio(1.3); + CHECK_MESSAGE( + path_follow_3d->get_progress_ratio() == 1, + "Progress Ratio should be clamped at 1"); + + memdelete(path); +} + +TEST_CASE("[PathFollow3D] Progress out of range") { + const Ref<Curve3D> &curve = memnew(Curve3D()); + curve->add_point(Vector3(0, 0, 0)); + curve->add_point(Vector3(100, 0, 0)); + const Path3D *path = memnew(Path3D); + path->set_curve(curve); + const PathFollow3D *path_follow_3d = memnew(PathFollow3D); + path->add_child(path_follow_3d); + + path_follow_3d->set_loop(true); + + path_follow_3d->set_progress(-50); + CHECK_MESSAGE( + path_follow_3d->get_progress() == 50, + "Progress should loop back from the end in the opposite direction"); + + path_follow_3d->set_progress(150); + CHECK_MESSAGE( + path_follow_3d->get_progress() == 50, + "Progress should loop back from the end in the opposite direction"); + + path_follow_3d->set_loop(false); + + path_follow_3d->set_progress(-50); + CHECK_MESSAGE( + path_follow_3d->get_progress() == 0, + "Progress should be clamped at 0"); + + path_follow_3d->set_progress(150); + CHECK_MESSAGE( + path_follow_3d->get_progress() == 100, + "Progress should be clamped at max value of curve"); + + memdelete(path); +} +} // namespace TestPathFollow3D + +#endif // TEST_PATH_FOLLOW_3D_H diff --git a/tests/scene/test_sprite_frames.h b/tests/scene/test_sprite_frames.h new file mode 100644 index 0000000000..61bbd16493 --- /dev/null +++ b/tests/scene/test_sprite_frames.h @@ -0,0 +1,247 @@ +/*************************************************************************/ +/* test_sprite_frames.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_SPRITE_FRAMES_H +#define TEST_SPRITE_FRAMES_H + +#include "scene/resources/sprite_frames.h" + +#include "tests/test_macros.h" + +namespace TestSpriteFrames { +const String test_animation_name = "GodotTest"; + +TEST_CASE("[SpriteFrames] Constructor methods") { + const SpriteFrames frames; + CHECK_MESSAGE( + frames.get_animation_names().size() == 1, + "Should be initialized with 1 entry."); + CHECK_MESSAGE( + frames.get_animation_names().get(0) == "default", + "Should be initialized with default entry."); +} + +TEST_CASE("[SpriteFrames] Animation addition, list getter, renaming, removal, and retrieval") { + SpriteFrames frames; + Vector<String> test_names = { "default", "2", "1", "3" }; + + // "default" is there already + frames.add_animation("2"); + frames.add_animation("1"); + frames.add_animation("3"); + + for (int i = 0; i < test_names.size(); i++) { + CHECK_MESSAGE( + frames.has_animation(test_names[i]), + "Add animation properly worked for each value"); + } + + CHECK_MESSAGE( + !frames.has_animation("999"), + "Return false when checking for animation that does not exist"); + + List<StringName> sname_list; + frames.get_animation_list(&sname_list); + + CHECK_MESSAGE( + sname_list.size() == test_names.size(), + "StringName List getter returned list of expected size"); + + for (int i = 0; i < test_names.size(); i++) { + CHECK_MESSAGE( + sname_list[i] == StringName(test_names[i]), + "StringName List getter returned expected values"); + } + + // get_animation_names() sorts the results. + Vector<String> string_vector = frames.get_animation_names(); + test_names.sort(); + + for (int i = 0; i < test_names.size(); i++) { + CHECK_MESSAGE( + string_vector[i] == test_names[i], + "String Vector getter returned expected values"); + } + + // These error handling cases should not crash. + ERR_PRINT_OFF; + frames.rename_animation("This does not exist", "0"); + ERR_PRINT_ON; + + CHECK_MESSAGE( + !frames.has_animation("0"), + "Correctly handles rename error when entry does not exist"); + + // These error handling cases should not crash. + ERR_PRINT_OFF; + frames.rename_animation("3", "1"); + ERR_PRINT_ON; + + CHECK_MESSAGE( + frames.has_animation("3"), + "Correctly handles rename error when entry exists, but new name already exists"); + + ERR_PRINT_OFF; + frames.add_animation("1"); + ERR_PRINT_ON; + + CHECK_MESSAGE( + frames.get_animation_names().size() == 4, + "Correctly does not add when entry already exists"); + + frames.rename_animation("3", "9"); + + CHECK_MESSAGE( + frames.has_animation("9"), + "Animation renamed correctly"); + + frames.remove_animation("9"); + + CHECK_MESSAGE( + !frames.has_animation("9"), + "Animation removed correctly"); + + frames.clear_all(); + + CHECK_MESSAGE( + frames.get_animation_names().size() == 1, + "Clear all removed all animations and re-added the default animation entry"); +} + +TEST_CASE("[SpriteFrames] Animation Speed getter and setter") { + SpriteFrames frames; + + frames.add_animation(test_animation_name); + + CHECK_MESSAGE( + frames.get_animation_speed(test_animation_name) == 5.0, + "Sets new animation to default speed"); + + frames.set_animation_speed(test_animation_name, 123.0004); + + CHECK_MESSAGE( + frames.get_animation_speed(test_animation_name) == 123.0004, + "Sets animation to positive double"); + + // These error handling cases should not crash. + ERR_PRINT_OFF; + frames.get_animation_speed("This does not exist"); + frames.set_animation_speed("This does not exist", 100); + frames.set_animation_speed(test_animation_name, -999.999); + ERR_PRINT_ON; + + CHECK_MESSAGE( + frames.get_animation_speed(test_animation_name) == 123.0004, + "Prevents speed of animation being set to a negative value"); + + frames.set_animation_speed(test_animation_name, 0.0); + + CHECK_MESSAGE( + frames.get_animation_speed(test_animation_name) == 0.0, + "Sets animation to zero"); +} + +TEST_CASE("[SpriteFrames] Animation Loop getter and setter") { + SpriteFrames frames; + + frames.add_animation(test_animation_name); + + CHECK_MESSAGE( + frames.get_animation_loop(test_animation_name), + "Sets new animation to default loop value."); + + frames.set_animation_loop(test_animation_name, true); + + CHECK_MESSAGE( + frames.get_animation_loop(test_animation_name), + "Sets animation loop to true"); + + frames.set_animation_loop(test_animation_name, false); + + CHECK_MESSAGE( + !frames.get_animation_loop(test_animation_name), + "Sets animation loop to false"); + + // These error handling cases should not crash. + ERR_PRINT_OFF; + frames.get_animation_loop("This does not exist"); + frames.set_animation_loop("This does not exist", false); + ERR_PRINT_ON; +} + +// TODO +TEST_CASE("[SpriteFrames] Frame addition, removal, and retrieval") { + Ref<Texture2D> dummy_frame1; + dummy_frame1.instantiate(); + + SpriteFrames frames; + frames.add_animation(test_animation_name); + frames.add_animation("1"); + frames.add_animation("2"); + + CHECK_MESSAGE( + frames.get_frame_count(test_animation_name) == 0, + "Animation has a default frame count of 0"); + + frames.add_frame(test_animation_name, dummy_frame1, 0); + frames.add_frame(test_animation_name, dummy_frame1, 1); + frames.add_frame(test_animation_name, dummy_frame1, 2); + + CHECK_MESSAGE( + frames.get_frame_count(test_animation_name) == 3, + "Adds multiple frames"); + + frames.remove_frame(test_animation_name, 1); + frames.remove_frame(test_animation_name, 0); + + CHECK_MESSAGE( + frames.get_frame_count(test_animation_name) == 1, + "Removes multiple frames"); + + // These error handling cases should not crash. + ERR_PRINT_OFF; + frames.add_frame("does not exist", dummy_frame1, 0); + frames.remove_frame(test_animation_name, -99); + frames.remove_frame("does not exist", 0); + ERR_PRINT_ON; + + CHECK_MESSAGE( + frames.get_frame_count(test_animation_name) == 1, + "Handles bad values when adding or removing frames."); + + frames.clear(test_animation_name); + + CHECK_MESSAGE( + frames.get_frame_count(test_animation_name) == 0, + "Clears frames."); +} +} // namespace TestSpriteFrames + +#endif // TEST_SPRITE_FRAMES_H diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h new file mode 100644 index 0000000000..652cbed6e8 --- /dev/null +++ b/tests/scene/test_text_edit.h @@ -0,0 +1,4264 @@ +/*************************************************************************/ +/* test_text_edit.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_TEXT_EDIT_H +#define TEST_TEXT_EDIT_H + +#include "scene/gui/text_edit.h" + +#include "tests/test_macros.h" + +namespace TestTextEdit { + +TEST_CASE("[SceneTree][TextEdit] text entry") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + text_edit->grab_focus(); + + Array empty_signal_args; + empty_signal_args.push_back(Array()); + + SUBCASE("[TextEdit] text entry") { + SIGNAL_WATCH(text_edit, "text_set"); + SIGNAL_WATCH(text_edit, "text_changed"); + SIGNAL_WATCH(text_edit, "lines_edited_from"); + SIGNAL_WATCH(text_edit, "caret_changed"); + + Array args1; + args1.push_back(0); + args1.push_back(0); + Array lines_edited_args; + lines_edited_args.push_back(args1); + lines_edited_args.push_back(args1.duplicate()); + + SUBCASE("[TextEdit] clear and set text") { + // "text_changed" should not be emitted on clear / set. + text_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_line_count() == 1); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->set_text("test text"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test text"); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_line_count() == 1); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + + // Can undo / redo words when editable. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test text"); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Cannot undo when not-editable but should still clear. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test text"); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Clear. + text_edit->set_editable(false); + + Array lines_edited_clear_args; + Array new_args = args1.duplicate(); + new_args[0] = 1; + lines_edited_clear_args.push_back(new_args); + + text_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_clear_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->set_editable(true); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK_FALSE("text_set"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + + // Can still undo set_text. + text_edit->set_editable(false); + + text_edit->set_text("test text"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test text"); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->set_editable(true); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Any selections are removed. + text_edit->set_text("test text"); + MessageQueue::get_singleton()->flush(); + text_edit->select_all(); + SIGNAL_CHECK("caret_changed", empty_signal_args); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test text"); + CHECK(text_edit->get_caret_column() == 9); + CHECK(text_edit->has_selection()); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->set_text("test"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test"); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->select_all(); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("caret_changed", empty_signal_args); + CHECK(text_edit->has_selection()); + + text_edit->clear(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + } + + SUBCASE("[TextEdit] set and get line") { + // Set / Get line is 0 indexed. + text_edit->set_line(1, "test"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("text_set"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + + text_edit->set_line(0, "test"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test"); + CHECK(text_edit->get_line(0) == "test"); + CHECK(text_edit->get_line(1) == ""); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + SIGNAL_CHECK_FALSE("caret_changed"); + + // Setting to a longer line, caret and selections should be preserved. + text_edit->select_all(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->has_selection()); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + text_edit->set_line(0, "test text"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "test text"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "test"); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Setting to a shorter line, selection and caret should be adjusted. Also works if not editable. + text_edit->set_editable(false); + text_edit->set_line(0, "te"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "te"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "te"); + CHECK(text_edit->get_caret_column() == 2); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + // Undo / redo should work. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "test text"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "test"); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "te"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_column() == 2); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Out of range. + ERR_PRINT_OFF; + text_edit->set_line(-1, "test"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "te"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->set_line(1, "test"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "te"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + ERR_PRINT_ON; + } + + SUBCASE("[TextEdit] swap lines") { + ((Array)lines_edited_args[1])[1] = 1; + + text_edit->set_text("testing\nswap"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->set_caret_column(text_edit->get_line(0).length()); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + ((Array)lines_edited_args[1])[1] = 0; + Array swap_args; + swap_args.push_back(1); + swap_args.push_back(1); + lines_edited_args.push_back(swap_args); + lines_edited_args.push_back(swap_args); + + // Order does not matter. Should also work if not editable. + text_edit->set_editable(false); + text_edit->swap_lines(1, 0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "swap\ntesting"); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + lines_edited_args.reverse(); + + // Single undo/redo action + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + lines_edited_args.reverse(); + + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "swap\ntesting"); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Out of range. + ERR_PRINT_OFF; + text_edit->swap_lines(-1, 0); + CHECK(text_edit->get_text() == "swap\ntesting"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->swap_lines(0, -1); + CHECK(text_edit->get_text() == "swap\ntesting"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->swap_lines(2, 0); + CHECK(text_edit->get_text() == "swap\ntesting"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->swap_lines(0, 2); + CHECK(text_edit->get_text() == "swap\ntesting"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + ERR_PRINT_ON; + } + + SUBCASE("[TextEdit] insert line at") { + ((Array)lines_edited_args[1])[1] = 1; + + text_edit->set_text("testing\nswap"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + text_edit->select_all(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_to_line() == 1); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + // insert before should move caret and selecion, and works when not editable. + text_edit->set_editable(false); + lines_edited_args.remove_at(0); + text_edit->insert_line_at(0, "new"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "new\ntesting\nswap"); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == text_edit->get_line(2).size() - 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_to_line() == 2); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + // can undo/redo as single action + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 0; + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + CHECK(text_edit->has_selection()); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + ((Array)lines_edited_args[0])[0] = 0; + ((Array)lines_edited_args[0])[1] = 1; + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "new\ntesting\nswap"); + CHECK(text_edit->has_selection()); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Adding inside selection extends selection. + text_edit->select_all(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_to_line() == 2); + SIGNAL_CHECK_FALSE("caret_changed"); + + ((Array)lines_edited_args[0])[0] = 2; + ((Array)lines_edited_args[0])[1] = 3; + text_edit->insert_line_at(2, "after"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == text_edit->get_line(3).size() - 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_to_line() == 3); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Out of range. + ERR_PRINT_OFF; + text_edit->insert_line_at(-1, "after"); + CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->insert_line_at(4, "after"); + CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("text_set"); + ERR_PRINT_ON; + } + + SUBCASE("[TextEdit] insert line at caret") { + lines_edited_args.pop_back(); + ((Array)lines_edited_args[0])[1] = 1; + + text_edit->insert_text_at_caret("testing\nswap"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == text_edit->get_line(1).size() - 1); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->set_caret_line(0, false); + text_edit->set_caret_column(2); + SIGNAL_DISCARD("caret_changed"); + + ((Array)lines_edited_args[0])[1] = 0; + text_edit->insert_text_at_caret("mid"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "temidsting\nswap"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->select(0, 0, 0, text_edit->get_line(0).length()); + CHECK(text_edit->has_selection()); + lines_edited_args.push_back(args1.duplicate()); + + text_edit->set_editable(false); + text_edit->insert_text_at_caret("new line"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "new line\nswap"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).size() - 1); + CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "temidsting\nswap"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->has_selection()); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "new line\nswap"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + } + + SIGNAL_UNWATCH(text_edit, "text_set"); + SIGNAL_UNWATCH(text_edit, "text_changed"); + SIGNAL_UNWATCH(text_edit, "lines_edited_from"); + SIGNAL_UNWATCH(text_edit, "caret_changed"); + } + + SUBCASE("[TextEdit] indent level") { + CHECK(text_edit->get_indent_level(0) == 0); + CHECK(text_edit->get_first_non_whitespace_column(0) == 0); + + text_edit->set_line(0, "a"); + CHECK(text_edit->get_indent_level(0) == 0); + CHECK(text_edit->get_first_non_whitespace_column(0) == 0); + + text_edit->set_line(0, "\t"); + CHECK(text_edit->get_indent_level(0) == 4); + CHECK(text_edit->get_first_non_whitespace_column(0) == 1); + + text_edit->set_tab_size(8); + CHECK(text_edit->get_indent_level(0) == 8); + + text_edit->set_line(0, "\t a"); + CHECK(text_edit->get_first_non_whitespace_column(0) == 2); + CHECK(text_edit->get_indent_level(0) == 9); + } + + SUBCASE("[TextEdit] selection") { + SIGNAL_WATCH(text_edit, "text_set"); + SIGNAL_WATCH(text_edit, "text_changed"); + SIGNAL_WATCH(text_edit, "lines_edited_from"); + SIGNAL_WATCH(text_edit, "caret_changed"); + + Array args1; + args1.push_back(0); + args1.push_back(0); + Array lines_edited_args; + lines_edited_args.push_back(args1); + lines_edited_args.push_back(args1.duplicate()); + + SUBCASE("[TextEdit] select all") { + text_edit->select_all(); + CHECK_FALSE(text_edit->has_selection()); + ERR_PRINT_OFF; + CHECK(text_edit->get_selection_from_line() == -1); + CHECK(text_edit->get_selection_from_column() == -1); + CHECK(text_edit->get_selection_to_line() == -1); + CHECK(text_edit->get_selection_to_column() == -1); + CHECK(text_edit->get_selected_text() == ""); + ERR_PRINT_ON; + + text_edit->set_text("test\nselection"); + SEND_GUI_ACTION(text_edit, "ui_text_select_all"); + CHECK(text_edit->get_viewport()->is_input_handled()); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_selected_text() == "test\nselection"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 9); + CHECK(text_edit->get_selection_mode() == TextEdit::SelectionMode::SELECTION_MODE_SHIFT); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->set_selecting_enabled(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + + text_edit->select_all(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + } + + SUBCASE("[TextEdit] select word under caret") { + text_edit->set_text("\ntest test\ntest test"); + + text_edit->set_caret_column(0); + text_edit->set_caret_line(1); + + text_edit->add_caret(2, 0); + text_edit->add_caret(2, 2); + CHECK(text_edit->get_caret_count() == 3); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->select_word_under_caret(); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_selection_from_line(0) == 1); + CHECK(text_edit->get_selection_from_column(0) == 0); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_selection_from_line(1) == 2); + CHECK(text_edit->get_selection_from_column(1) == 0); + CHECK(text_edit->get_selection_to_line(1) == 2); + CHECK(text_edit->get_selection_to_column(1) == 4); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + + CHECK(text_edit->get_caret_count() == 2); + + text_edit->select_word_under_caret(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + + SEND_GUI_ACTION(text_edit, "ui_text_select_word_under_caret"); + CHECK(text_edit->get_viewport()->is_input_handled()); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_selection_from_line(0) == 1); + CHECK(text_edit->get_selection_from_column(0) == 0); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_selection_from_line(1) == 2); + CHECK(text_edit->get_selection_from_column(1) == 0); + CHECK(text_edit->get_selection_to_line(1) == 2); + CHECK(text_edit->get_selection_to_column(1) == 4); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + + CHECK(text_edit->get_selected_text() == "test\ntest"); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + text_edit->set_selecting_enabled(false); + text_edit->select_word_under_caret(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK_FALSE("caret_changed"); + text_edit->set_selecting_enabled(true); + + text_edit->set_caret_line(1, false, true, 0, 0); + text_edit->set_caret_column(5, false, 0); + + text_edit->set_caret_line(2, false, true, 0, 1); + text_edit->set_caret_column(5, false, 1); + + text_edit->select_word_under_caret(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + + text_edit->select_word_under_caret(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); + SIGNAL_CHECK_FALSE("caret_changed"); + } + + SUBCASE("[TextEdit] add selection for next occurrence") { + text_edit->set_text("\ntest other_test\nrandom test\nword test word"); + text_edit->set_caret_column(0); + text_edit->set_caret_line(1); + + text_edit->select_word_under_caret(); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->get_selection_from_line(1) == 1); + CHECK(text_edit->get_selection_from_column(1) == 13); + CHECK(text_edit->get_selection_to_line(1) == 1); + CHECK(text_edit->get_selection_to_column(1) == 17); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 17); + + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_selected_text(2) == "test"); + CHECK(text_edit->get_selection_from_line(2) == 2); + CHECK(text_edit->get_selection_from_column(2) == 9); + CHECK(text_edit->get_selection_to_line(2) == 2); + CHECK(text_edit->get_selection_to_column(2) == 13); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 13); + + text_edit->add_selection_for_next_occurrence(); + CHECK(text_edit->get_caret_count() == 4); + CHECK(text_edit->get_selected_text(3) == "test"); + CHECK(text_edit->get_selection_from_line(3) == 3); + CHECK(text_edit->get_selection_from_column(3) == 5); + CHECK(text_edit->get_selection_to_line(3) == 3); + CHECK(text_edit->get_selection_to_column(3) == 9); + CHECK(text_edit->get_caret_line(3) == 3); + CHECK(text_edit->get_caret_column(3) == 9); + } + + SUBCASE("[TextEdit] deselect on focus loss") { + text_edit->set_text("test"); + + text_edit->set_deselect_on_focus_loss_enabled(true); + CHECK(text_edit->is_deselect_on_focus_loss_enabled()); + + text_edit->grab_focus(); + text_edit->select_all(); + CHECK(text_edit->has_focus()); + CHECK(text_edit->has_selection()); + + text_edit->release_focus(); + CHECK_FALSE(text_edit->has_focus()); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->set_deselect_on_focus_loss_enabled(false); + CHECK_FALSE(text_edit->is_deselect_on_focus_loss_enabled()); + + text_edit->grab_focus(); + text_edit->select_all(); + CHECK(text_edit->has_focus()); + CHECK(text_edit->has_selection()); + + text_edit->release_focus(); + CHECK_FALSE(text_edit->has_focus()); + CHECK(text_edit->has_selection()); + + text_edit->set_deselect_on_focus_loss_enabled(true); + CHECK_FALSE(text_edit->has_selection()); + } + + SUBCASE("[TextEdit] key select") { + text_edit->set_text("test"); + + text_edit->grab_focus(); + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT) + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "t"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT | KeyModifierMask::ALT) +#else + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL) +#endif + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "test"); + + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT) + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "tes"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT | KeyModifierMask::ALT) +#else + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL) +#endif + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT) + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "t"); + + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT) + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT) + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "t"); + + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT) + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + + text_edit->set_selecting_enabled(false); + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT) + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == ""); + text_edit->set_selecting_enabled(true); + } + + SUBCASE("[TextEdit] mouse drag select") { + /* Set size for mouse input. */ + text_edit->set_size(Size2(200, 200)); + + text_edit->set_text("this is some text\nfor selection"); + text_edit->grab_focus(); + MessageQueue::get_singleton()->flush(); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for s"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 5); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->set_selecting_enabled(false); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + text_edit->set_selecting_enabled(true); + } + + SUBCASE("[TextEdit] mouse word select") { + /* Set size for mouse input. */ + text_edit->set_size(Size2(200, 200)); + + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 3); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 13); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 13); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); + line_0.y /= 2; + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->set_selecting_enabled(false); + SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 3); + text_edit->set_selecting_enabled(true); + } + + SUBCASE("[TextEdit] mouse line select") { + /* Set size for mouse input. */ + text_edit->set_size(Size2(200, 200)); + + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + + SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 13); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + + Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); + line_0.y /= 2; + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->set_selecting_enabled(false); + SEND_GUI_DOUBLE_CLICK(text_edit, text_edit->get_pos_at_line_column(0, 2), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + text_edit->set_selecting_enabled(true); + } + + SUBCASE("[TextEdit] mouse shift click select") { + /* Set size for mouse input. */ + text_edit->set_size(Size2(200, 200)); + + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE | KeyModifierMask::SHIFT); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for s"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 5); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->set_selecting_enabled(false); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE | KeyModifierMask::SHIFT); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + text_edit->set_selecting_enabled(true); + } + + SUBCASE("[TextEdit] select and deselect") { + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + + text_edit->select(-1, -1, 500, 500); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + + text_edit->deselect(); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->select(500, 500, -1, -1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + + text_edit->deselect(); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->select(0, 4, 0, 8); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == " is "); + + text_edit->deselect(); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->select(0, 8, 0, 4); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == " is "); + + text_edit->set_selecting_enabled(false); + CHECK_FALSE(text_edit->has_selection()); + text_edit->select(0, 8, 0, 4); + CHECK_FALSE(text_edit->has_selection()); + text_edit->set_selecting_enabled(true); + + text_edit->select(0, 8, 0, 4); + CHECK(text_edit->has_selection()); + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK_FALSE(text_edit->has_selection()); + + text_edit->delete_selection(); + CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + + text_edit->select(0, 8, 0, 4); + CHECK(text_edit->has_selection()); + SEND_GUI_ACTION(text_edit, "ui_text_backspace"); + CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + + text_edit->undo(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + + text_edit->redo(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + + text_edit->undo(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + + text_edit->select(0, 8, 0, 4); + CHECK(text_edit->has_selection()); + + text_edit->delete_selection(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "thissome text\nfor selection"); + + text_edit->undo(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + + text_edit->redo(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + + text_edit->undo(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + + text_edit->set_editable(false); + text_edit->delete_selection(); + text_edit->set_editable(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "thissome text\nfor selection"); + + text_edit->undo(); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "thissome text\nfor selection"); + } + + // Add readonly test? + SUBCASE("[TextEdit] text drag") { + TextEdit *target_text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(target_text_edit); + text_edit->get_viewport()->set_embedding_subwindows(true); // Bypass display server for drop handling. + + target_text_edit->set_size(Size2(200, 200)); + target_text_edit->set_position(Point2(400, 0)); + + text_edit->set_size(Size2(200, 200)); + + CHECK_FALSE(text_edit->is_mouse_over_selection()); + text_edit->set_text("drag me"); + text_edit->select_all(); + text_edit->grab_click_focus(); + MessageQueue::get_singleton()->flush(); + + Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); + line_0.y /= 2; + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit, text_edit->get_pos_at_line_column(0, 7), MouseButton::MASK_LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag me"); + + line_0 = target_text_edit->get_pos_at_line_column(0, 0); + line_0.y /= 2; + line_0.x += 401; // As empty add one. + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit, line_0, MouseButton::MASK_LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit, line_0, MouseButton::LEFT, MouseButton::MASK_LEFT, Key::NONE); + + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == ""); + CHECK(target_text_edit->get_text() == "drag me"); + + memdelete(target_text_edit); + } + + SIGNAL_UNWATCH(text_edit, "text_set"); + SIGNAL_UNWATCH(text_edit, "text_changed"); + SIGNAL_UNWATCH(text_edit, "lines_edited_from"); + SIGNAL_UNWATCH(text_edit, "caret_changed"); + } + + SUBCASE("[TextEdit] overridable actions") { + SIGNAL_WATCH(text_edit, "text_set"); + SIGNAL_WATCH(text_edit, "text_changed"); + SIGNAL_WATCH(text_edit, "lines_edited_from"); + SIGNAL_WATCH(text_edit, "caret_changed"); + + Array args1; + args1.push_back(0); + args1.push_back(0); + Array lines_edited_args; + lines_edited_args.push_back(args1); + + SUBCASE("[TextEdit] backspace") { + text_edit->set_text("this is\nsome\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->backspace(); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + text_edit->set_caret_line(2); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + ((Array)lines_edited_args[0])[0] = 2; + ((Array)lines_edited_args[0])[1] = 1; + text_edit->backspace(); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->get_text() == "this is\nsome"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + ((Array)lines_edited_args[0])[0] = 1; + text_edit->backspace(); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->get_text() == "this is\nsom"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->end_complex_operation(); + text_edit->select(1, 0, 1, 3); + text_edit->backspace(); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->get_text() == "this is\n"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_editable(false); + text_edit->backspace(); + text_edit->set_editable(true); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->get_text() == "this is\n"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is\nsom"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SUBCASE("[TextEdit] cut") { + text_edit->set_text("this is\nsome\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(6); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + ERR_PRINT_OFF; + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + ERR_PRINT_ON; // Can't check display server content. + + ((Array)lines_edited_args[0])[0] = 1; + CHECK(text_edit->get_text() == "some\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + ((Array)lines_edited_args[0])[0] = 0; + ((Array)lines_edited_args[0])[1] = 1; + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is\nsome\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 0; + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "some\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_text("this is\nsome\n"); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + ((Array)lines_edited_args[0])[0] = 0; + text_edit->select(0, 5, 0, 7); + ERR_PRINT_OFF; + SEND_GUI_ACTION(text_edit, "ui_cut"); + CHECK(text_edit->get_viewport()->is_input_handled()); + MessageQueue::get_singleton()->flush(); + ERR_PRINT_ON; // Can't check display server content. + CHECK(text_edit->get_text() == "this \nsome\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_editable(false); + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + text_edit->set_editable(true); + CHECK(text_edit->get_text() == "this \nsome\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] copy") { + // TODO: Cannot test need display server support. + } + + SUBCASE("[TextEdit] paste") { + // TODO: Cannot test need display server support. + } + + SUBCASE("[TextEdit] paste primary") { + // TODO: Cannot test need display server support. + } + + SIGNAL_UNWATCH(text_edit, "text_set"); + SIGNAL_UNWATCH(text_edit, "text_changed"); + SIGNAL_UNWATCH(text_edit, "lines_edited_from"); + SIGNAL_UNWATCH(text_edit, "caret_changed"); + } + + // Add undo / redo tests? + SUBCASE("[TextEdit] input") { + SIGNAL_WATCH(text_edit, "text_set"); + SIGNAL_WATCH(text_edit, "text_changed"); + SIGNAL_WATCH(text_edit, "lines_edited_from"); + SIGNAL_WATCH(text_edit, "caret_changed"); + + Array args1; + args1.push_back(0); + args1.push_back(0); + Array lines_edited_args; + lines_edited_args.push_back(args1); + + SUBCASE("[TextEdit] ui_text_newline_above") { + text_edit->set_text("this is some test text.\nthis is some test text."); + text_edit->select(0, 0, 0, 4); + text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(0); + args2.push_back(1); + lines_edited_args.push_front(args2); + + ((Array)lines_edited_args[1])[1] = 1; + SEND_GUI_ACTION(text_edit, "ui_text_newline_above"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + + text_edit->set_caret_line(3, false, true, 0, 1); + text_edit->set_caret_column(4, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_newline_above"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 4); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + ((Array)lines_edited_args[0])[0] = 2; + ((Array)lines_edited_args[0])[1] = 3; + + SEND_GUI_ACTION(text_edit, "ui_text_newline_above"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n\nthis is some test text.\n\n\nthis is some test text."); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 4); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SUBCASE("[TextEdit] ui_text_newline_blank") { + text_edit->set_text("this is some test text.\nthis is some test text."); + text_edit->select(0, 0, 0, 4); + text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(2); + lines_edited_args.push_front(args2); + + ((Array)lines_edited_args[1])[1] = 1; + SEND_GUI_ACTION(text_edit, "ui_text_newline_blank"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_newline_blank"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + } + + SUBCASE("[TextEdit] ui_text_newline") { + text_edit->set_text("this is some test text.\nthis is some test text."); + text_edit->select(0, 0, 0, 4); + text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + lines_edited_args.push_front(args2.duplicate()); + ((Array)lines_edited_args[1])[1] = 2; + + lines_edited_args.push_back(lines_edited_args[2].duplicate()); + ((Array)lines_edited_args[3])[1] = 1; + + SEND_GUI_ACTION(text_edit, "ui_text_newline"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_newline"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + } + + SUBCASE("[TextEdit] ui_text_backspace_all_to_left") { + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); + text_edit->select(1, 0, 1, 4); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + + text_edit->add_caret(3, 4); + text_edit->select(3, 0, 3, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL); + InputMap::get_singleton()->action_add_event("ui_text_backspace_all_to_left", tmpevent); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(3); + args2.push_back(3); + lines_edited_args.push_front(args2); + + // With selection should be a normal backspace. + ((Array)lines_edited_args[1])[0] = 1; + ((Array)lines_edited_args[1])[1] = 1; + + SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + ((Array)lines_edited_args[0])[1] = 2; + ((Array)lines_edited_args[1])[1] = 0; + + // Start of line should also be a normal backspace. + SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 0; + + SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + InputMap::get_singleton()->action_erase_event("ui_text_backspace_all_to_left", tmpevent); + } + + SUBCASE("[TextEdit] ui_text_backspace_word") { + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); + text_edit->select(1, 0, 1, 4); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + + text_edit->add_caret(3, 4); + text_edit->select(3, 0, 3, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(3); + args2.push_back(3); + lines_edited_args.push_front(args2); + + // With selection should be a normal backspace. + ((Array)lines_edited_args[1])[0] = 1; + ((Array)lines_edited_args[1])[1] = 1; + + SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->end_complex_operation(); + + ((Array)lines_edited_args[0])[1] = 2; + ((Array)lines_edited_args[1])[1] = 0; + + // Start of line should also be a normal backspace. + SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 0; + + SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test \n is some test "); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 14); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 14); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SUBCASE("[TextEdit] ui_text_backspace") { + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); + text_edit->select(1, 0, 1, 4); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + + text_edit->add_caret(3, 4); + text_edit->select(3, 0, 3, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(3); + args2.push_back(3); + lines_edited_args.push_front(args2); + + // With selection should be a normal backspace. + ((Array)lines_edited_args[1])[0] = 1; + ((Array)lines_edited_args[1])[1] = 1; + + SEND_GUI_ACTION(text_edit, "ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + ((Array)lines_edited_args[0])[1] = 2; + ((Array)lines_edited_args[1])[1] = 0; + + // Start of line should also be a normal backspace. + SEND_GUI_ACTION(text_edit, "ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 0; + + SEND_GUI_ACTION(text_edit, "ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text\n is some test text"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 18); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 18); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Select the entire text, from right to left + text_edit->select(0, 18, 0, 0); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + text_edit->select(1, 18, 1, 0, 1); + text_edit->set_caret_line(1, false, true, 0, 1); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION(text_edit, "ui_text_backspace"); + CHECK(text_edit->get_text() == "\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SUBCASE("[TextEdit] ui_text_delete_all_to_right") { + Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL); + InputMap::get_singleton()->action_add_event("ui_text_delete_all_to_right", tmpevent); + + text_edit->set_text("this is some test text.\nthis is some test text.\n"); + text_edit->select(0, 0, 0, 4); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + + // With selection should be a normal delete. + SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // End of line should not do anything. + text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + InputMap::get_singleton()->action_erase_event("ui_text_delete_all_to_right", tmpevent); + } + + SUBCASE("[TextEdit] ui_text_delete_word") { + text_edit->set_caret_mid_grapheme_enabled(true); + CHECK(text_edit->is_caret_mid_grapheme_enabled()); + + text_edit->set_text("this ffi some test text.\n\nthis ffi some test text.\n"); + text_edit->select(0, 0, 0, 4); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + + text_edit->add_caret(2, 4); + text_edit->select(2, 0, 2, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(2); + args2.push_back(2); + lines_edited_args.push_front(args2); + + // With selection should be a normal delete. + SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " ffi some test text.\n\n ffi some test text.\n"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // With selection should be a normal delete. + ((Array)lines_edited_args[0])[0] = 3; + ((Array)lines_edited_args[1])[0] = 1; + text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(2).length(), false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection()); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + ((Array)lines_edited_args[1])[0] = 0; + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " some test text.\n some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SUBCASE("[TextEdit] ui_text_delete") { + text_edit->set_caret_mid_grapheme_enabled(true); + CHECK(text_edit->is_caret_mid_grapheme_enabled()); + + text_edit->set_text("this ffi some test text.\nthis ffi some test text."); + text_edit->select(0, 0, 0, 4); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + + // With selection should be a normal delete. + SEND_GUI_ACTION(text_edit, "ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // With selection should be a normal delete. + lines_edited_args.remove_at(0); + ((Array)lines_edited_args[0])[0] = 1; + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + text_edit->set_caret_column(text_edit->get_line(0).length()); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION(text_edit, "ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " ffi some test text. ffi some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 20); + CHECK_FALSE(text_edit->has_selection(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Caret should be removed due to column preservation. + CHECK(text_edit->get_caret_count() == 1); + + // Lets add it back. + text_edit->set_caret_column(0); + text_edit->add_caret(0, 20); + + ((Array)lines_edited_args[0])[0] = 0; + lines_edited_args.push_back(args2); + ((Array)lines_edited_args[1])[0] = 0; + ((Array)lines_edited_args[1])[1] = 0; + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION(text_edit, "ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " ffi some test text. ffi some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 20); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + + SEND_GUI_ACTION(text_edit, "ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "ffi some test text.ffi some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 19); + CHECK_FALSE(text_edit->has_selection(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + + SEND_GUI_ACTION(text_edit, "ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "fi some test text.fi some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 18); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_caret_mid_grapheme_enabled(false); + CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled()); + + text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "ffi some test text.ffi some test text."); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION(text_edit, "ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " some test text. some test text."); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 16); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SUBCASE("[TextEdit] ui_text_caret_word_left") { + text_edit->set_text("\nthis is some test text.\nthis is some test text."); + text_edit->set_caret_line(1); + text_edit->set_caret_column(7); + + text_edit->add_caret(2, 7); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Shift should select. +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_selected_text(0) == "is"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selected_text(1) == "is"); + CHECK(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Should still move caret with selection. + SEND_GUI_ACTION(text_edit, "ui_text_caret_word_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Normal word left. + SEND_GUI_ACTION(text_edit, "ui_text_caret_word_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 23); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_left") { + text_edit->set_text("\nthis is some test text.\nthis is some test text."); + text_edit->set_caret_line(1); + text_edit->set_caret_column(7); + text_edit->select(1, 2, 1, 7); + + text_edit->add_caret(2, 7); + text_edit->select(2, 2, 2, 7, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Normal left shoud deselect and place at selection start. + SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // With shift should select. + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_selected_text(0) == "h"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->get_selected_text(1) == "h"); + CHECK(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // All ready at select left, should only deselect. + SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 1); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK_FALSE(text_edit->has_selection(1)); + + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Normal left. + SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Left at col 0 should go up a line. + SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 23); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_word_right") { + text_edit->set_text("this is some test text\n\nthis is some test text\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(13); + + text_edit->add_caret(2, 13); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Shift should select. +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 17); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 17); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Should still move caret with selection. + SEND_GUI_ACTION(text_edit, "ui_text_caret_word_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 22); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 22); + CHECK_FALSE(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Normal word right. + SEND_GUI_ACTION(text_edit, "ui_text_caret_word_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_right") { + text_edit->set_text("this is some test text\n\nthis is some test text\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(16); + text_edit->select(0, 16, 0, 20); + + text_edit->add_caret(2, 16); + text_edit->select(2, 16, 2, 20, 1); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Normal right shoud deselect and place at selection start. + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 20); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 20); + CHECK_FALSE(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // With shift should select. + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 21); + CHECK(text_edit->get_selected_text(0) == "x"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 21); + CHECK(text_edit->get_selected_text(1) == "x"); + CHECK(text_edit->has_selection(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // All ready at select right, should only deselect. + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 21); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 21); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Normal right. + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 22); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 22); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Right at end col should go down a line. + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_up") { + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + + text_edit->set_size(Size2(110, 100)); + text_edit->set_text("this is some\nother test\nlines\ngo here\nthis is some\nother test\nlines\ngo here"); + text_edit->set_caret_line(3); + text_edit->set_caret_column(7); + + text_edit->add_caret(7, 7); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->is_line_wrapped(0)); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Select + up should select everything to the left on that line. + SEND_GUI_KEY_EVENT(text_edit, Key::UP | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_selected_text(0) == "\ngo here"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 6); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selected_text(1) == "\ngo here"); + CHECK(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Should deselect and move up. + SEND_GUI_ACTION(text_edit, "ui_text_caret_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 8); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 5); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Normal up over wrapped line. + SEND_GUI_ACTION(text_edit, "ui_text_caret_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 12); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 4); + CHECK(text_edit->get_caret_column(1) == 12); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_caret_column(12, false); + + // Normal up over wrapped line to line 0. + SEND_GUI_ACTION(text_edit, "ui_text_caret_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 4); + CHECK(text_edit->get_caret_column(1) == 7); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_down") { + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + + text_edit->set_size(Size2(110, 100)); + text_edit->set_text("go here\nlines\nother test\nthis is some\ngo here\nlines\nother test\nthis is some"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(7); + + text_edit->add_caret(4, 7); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->is_line_wrapped(3)); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Select + down should select everything to the right on that line. + SEND_GUI_KEY_EVENT(text_edit, Key::DOWN | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_selected_text(0) == "\nlines"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 5); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selected_text(1) == "\nlines"); + CHECK(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Should deselect and move down. + SEND_GUI_ACTION(text_edit, "ui_text_caret_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 8); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 6); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Normal down over wrapped line. + SEND_GUI_ACTION(text_edit, "ui_text_caret_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 7); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 7); + CHECK(text_edit->get_caret_column(1) == 7); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_caret_column(7, false); + + // Normal down over wrapped line to last wrapped line. + SEND_GUI_ACTION(text_edit, "ui_text_caret_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 12); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 7); + CHECK(text_edit->get_caret_column(1) == 12); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_document_start") { + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + + text_edit->set_size(Size2(110, 100)); + text_edit->set_text("this is some\nother test\nlines\ngo here"); + text_edit->set_caret_line(4); + text_edit->set_caret_column(7); + + text_edit->add_caret(3, 2); + CHECK(text_edit->get_caret_count() == 2); + + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->is_line_wrapped(0)); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::UP | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(text_edit, Key::HOME | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selected_text() == "this is some\nother test\nlines\ngo here"); + CHECK(text_edit->has_selection()); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + CHECK(text_edit->get_caret_count() == 1); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_document_start"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_document_end") { + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + + text_edit->set_size(Size2(110, 100)); + text_edit->set_text("go here\nlines\nother test\nthis is some"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + text_edit->add_caret(1, 0); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->is_line_wrapped(3)); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::DOWN | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(text_edit, Key::END | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some"); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 12); + CHECK(text_edit->get_selected_text() == "go here\nlines\nother test\nthis is some"); + CHECK(text_edit->has_selection()); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + CHECK(text_edit->get_caret_count() == 1); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_document_end"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some"); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 12); + CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_line_start") { + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + + text_edit->set_size(Size2(110, 100)); + text_edit->set_text(" this is some\n this is some"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(text_edit->get_line(0).length()); + + text_edit->add_caret(1, text_edit->get_line(1).length()); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->is_line_wrapped(0)); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(text_edit, Key::HOME | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 10); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "some"); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "some"); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_line_start"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_line_start"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_line_start"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] ui_text_caret_line_end") { + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + + text_edit->set_size(Size2(110, 100)); + text_edit->set_text(" this is some\n this is some"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + text_edit->add_caret(1, 0); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->is_line_wrapped(0)); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(text_edit, Key::END | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 9); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == " this is"); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == " this is"); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_line_end"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + } + + SUBCASE("[TextEdit] unicode") { + text_edit->set_text("\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + text_edit->add_caret(1, 0); + CHECK(text_edit->get_caret_count() == 2); + text_edit->insert_text_at_caret("a"); + MessageQueue::get_singleton()->flush(); + + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + + SEND_GUI_KEY_EVENT(text_edit, Key::A); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "aA\naA"); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_caret_column(1) == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->set_editable(false); + SEND_GUI_KEY_EVENT(text_edit, Key::A); + CHECK_FALSE(text_edit->get_viewport()->is_input_handled()); // Should this be handled? + CHECK(text_edit->get_text() == "aA\naA"); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_caret_column(1) == 2); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->set_editable(true); + + lines_edited_args.push_back(lines_edited_args[1].duplicate()); + lines_edited_args.push_front(args2.duplicate()); + + text_edit->select(0, 0, 0, 1); + text_edit->select(1, 0, 1, 1, 1); + SEND_GUI_KEY_EVENT(text_edit, Key::B); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "BA\nBA"); + CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_caret_column(1) == 1); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + SEND_GUI_ACTION(text_edit, "ui_text_toggle_insert_mode"); + CHECK(text_edit->is_overtype_mode_enabled()); + + SEND_GUI_KEY_EVENT(text_edit, Key::B); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "BB\nBB"); + CHECK(text_edit->get_caret_column() == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + text_edit->select(0, 0, 0, 1); + text_edit->select(1, 0, 1, 1, 1); + SEND_GUI_KEY_EVENT(text_edit, Key::A); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "AB\nAB"); + CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_caret_column(1) == 1); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->set_overtype_mode_enabled(false); + CHECK_FALSE(text_edit->is_overtype_mode_enabled()); + + lines_edited_args.remove_at(0); + lines_edited_args.remove_at(1); + + SEND_GUI_KEY_EVENT(text_edit, Key::TAB); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "A\tB\nA\tB"); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_caret_column(1) == 2); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + } + + SIGNAL_UNWATCH(text_edit, "text_set"); + SIGNAL_UNWATCH(text_edit, "text_changed"); + SIGNAL_UNWATCH(text_edit, "lines_edited_from"); + SIGNAL_UNWATCH(text_edit, "caret_changed"); + } + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] context menu") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + text_edit->get_viewport()->set_embedding_subwindows(true); // Bypass display server for drop handling. + + text_edit->set_size(Size2(800, 200)); + text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); + MessageQueue::get_singleton()->flush(); + + text_edit->set_context_menu_enabled(false); + CHECK_FALSE(text_edit->is_context_menu_enabled()); + + CHECK_FALSE(text_edit->is_menu_visible()); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(600, 10), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE); + CHECK_FALSE(text_edit->is_menu_visible()); + + text_edit->set_context_menu_enabled(true); + CHECK(text_edit->is_context_menu_enabled()); + + CHECK_FALSE(text_edit->is_menu_visible()); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(700, 10), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE); + CHECK(text_edit->is_menu_visible()); + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] versioning") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + // Action undo / redo states are tested in the action test e.g selection_delete. + CHECK_FALSE(text_edit->has_undo()); + CHECK_FALSE(text_edit->has_redo()); + CHECK(text_edit->get_version() == 0); + CHECK(text_edit->get_saved_version() == 0); + + text_edit->begin_complex_operation(); + text_edit->begin_complex_operation(); + text_edit->begin_complex_operation(); + + text_edit->insert_text_at_caret("test"); + CHECK(text_edit->get_version() == 1); + CHECK(text_edit->get_saved_version() == 0); + CHECK(text_edit->has_undo()); + CHECK_FALSE(text_edit->has_redo()); + + text_edit->end_complex_operation(); + + // Can undo and redo mid op. + text_edit->insert_text_at_caret(" nested"); + CHECK(text_edit->get_version() == 2); + CHECK(text_edit->get_saved_version() == 0); + CHECK(text_edit->has_undo()); + CHECK_FALSE(text_edit->has_redo()); + text_edit->undo(); + + CHECK(text_edit->has_redo()); + text_edit->redo(); + + text_edit->end_complex_operation(); + + text_edit->insert_text_at_caret(" ops"); + CHECK(text_edit->get_version() == 3); + CHECK(text_edit->get_saved_version() == 0); + CHECK(text_edit->has_undo()); + CHECK_FALSE(text_edit->has_redo()); + + text_edit->end_complex_operation(); + + text_edit->tag_saved_version(); + CHECK(text_edit->get_saved_version() == 3); + + text_edit->undo(); + CHECK(text_edit->get_line(0) == ""); + CHECK(text_edit->get_version() == 0); + CHECK(text_edit->get_saved_version() == 3); + CHECK_FALSE(text_edit->has_undo()); + CHECK(text_edit->has_redo()); + + text_edit->redo(); + CHECK(text_edit->get_line(0) == "test ops nested"); + CHECK(text_edit->get_version() == 3); + CHECK(text_edit->get_saved_version() == 3); + CHECK(text_edit->has_undo()); + CHECK_FALSE(text_edit->has_redo()); + + text_edit->clear_undo_history(); + CHECK_FALSE(text_edit->has_undo()); + CHECK_FALSE(text_edit->has_redo()); + CHECK(text_edit->get_version() == 3); // Should this be cleared? + CHECK(text_edit->get_saved_version() == 0); + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] search") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + text_edit->set_text("hay needle, hay\nHAY NEEDLE, HAY"); + int length = text_edit->get_line(1).length(); + + CHECK(text_edit->search("test", 0, 0, 0) == Point2i(-1, -1)); + CHECK(text_edit->search("test", TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(-1, -1)); + CHECK(text_edit->search("test", TextEdit::SEARCH_WHOLE_WORDS, 0, 0) == Point2i(-1, -1)); + CHECK(text_edit->search("test", TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(-1, -1)); + + CHECK(text_edit->search("test", 0, 1, length) == Point2i(-1, -1)); + CHECK(text_edit->search("test", TextEdit::SEARCH_MATCH_CASE, 1, length) == Point2i(-1, -1)); + CHECK(text_edit->search("test", TextEdit::SEARCH_WHOLE_WORDS, 1, length) == Point2i(-1, -1)); + CHECK(text_edit->search("test", TextEdit::SEARCH_BACKWARDS, 1, length) == Point2i(-1, -1)); + + CHECK(text_edit->search("needle", 0, 0, 0) == Point2i(4, 0)); + CHECK(text_edit->search("needle", 0, 1, length) == Point2i(4, 0)); + CHECK(text_edit->search("needle", 0, 0, 5) == Point2i(4, 1)); + CHECK(text_edit->search("needle", TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 1)); + CHECK(text_edit->search("needle", TextEdit::SEARCH_BACKWARDS, 1, 5) == Point2i(4, 1)); + CHECK(text_edit->search("needle", TextEdit::SEARCH_BACKWARDS, 1, 3) == Point2i(4, 0)); + + CHECK(text_edit->search("needle", TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(4, 0)); + CHECK(text_edit->search("needle", TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 0)); + + CHECK(text_edit->search("needle", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(4, 0)); + CHECK(text_edit->search("needle", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 0)); + + CHECK(text_edit->search("need", TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(4, 0)); + CHECK(text_edit->search("need", TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(4, 0)); + + CHECK(text_edit->search("need", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE, 0, 0) == Point2i(-1, -1)); + CHECK(text_edit->search("need", TextEdit::SEARCH_WHOLE_WORDS | TextEdit::SEARCH_MATCH_CASE | TextEdit::SEARCH_BACKWARDS, 0, 0) == Point2i(-1, -1)); + + ERR_PRINT_OFF; + CHECK(text_edit->search("", 0, 0, 0) == Point2i(-1, -1)); + CHECK(text_edit->search("needle", 0, -1, 0) == Point2i(-1, -1)); + CHECK(text_edit->search("needle", 0, 0, -1) == Point2i(-1, -1)); + CHECK(text_edit->search("needle", 0, 100, 0) == Point2i(-1, -1)); + CHECK(text_edit->search("needle", 0, 0, 100) == Point2i(-1, -1)); + ERR_PRINT_ON; + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] mouse") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + text_edit->set_size(Size2(800, 200)); + text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->get_word_at_pos(text_edit->get_pos_at_line_column(0, 1)) == "Lorem"); + CHECK(text_edit->get_word_at_pos(text_edit->get_pos_at_line_column(0, 9)) == "ipsum"); + + ERR_PRINT_OFF; + CHECK(text_edit->get_pos_at_line_column(0, -1) == Point2i(-1, -1)); + CHECK(text_edit->get_pos_at_line_column(-1, 0) == Point2i(-1, -1)); + CHECK(text_edit->get_pos_at_line_column(-1, -1) == Point2i(-1, -1)); + + CHECK(text_edit->get_pos_at_line_column(0, 500) == Point2i(-1, -1)); + CHECK(text_edit->get_pos_at_line_column(2, 0) == Point2i(-1, -1)); + CHECK(text_edit->get_pos_at_line_column(2, 500) == Point2i(-1, -1)); + + // Out of view. + CHECK(text_edit->get_pos_at_line_column(0, text_edit->get_line(0).length() - 1) == Point2i(-1, -1)); + ERR_PRINT_ON; + + // Add method to get drawn column count? + Point2i start_pos = text_edit->get_pos_at_line_column(0, 0); + Point2i end_pos = text_edit->get_pos_at_line_column(0, 105); + + CHECK(text_edit->get_line_column_at_pos(Point2i(start_pos.x, start_pos.y)) == Point2i(0, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y)) == Point2i(104, 0)); + + // Should this return Point2i(-1, -1) if its also < 0 not just > vis_lines. + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y), false) == Point2i(90, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y + 100), false) == Point2i(-1, -1)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y + 100), false) == Point2i(-1, -1)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y - 100), false) == Point2i(104, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y - 100), false) == Point2i(90, 0)); + + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y)) == Point2i(90, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y + 100)) == Point2i(141, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y + 100)) == Point2i(141, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x, end_pos.y - 100)) == Point2i(104, 0)); + CHECK(text_edit->get_line_column_at_pos(Point2i(end_pos.x - 100, end_pos.y - 100)) == Point2i(90, 0)); + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] caret") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + text_edit->set_size(Size2(800, 200)); + text_edit->grab_focus(); + text_edit->set_line(0, "ffi"); + + text_edit->set_caret_mid_grapheme_enabled(true); + CHECK(text_edit->is_caret_mid_grapheme_enabled()); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_caret_column() == 1); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_caret_column() == 2); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_caret_column() == 3); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); + CHECK(text_edit->get_caret_column() == 2); + + text_edit->set_caret_mid_grapheme_enabled(false); + CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled()); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); + CHECK(text_edit->get_caret_column() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); + CHECK(text_edit->get_caret_column() == 3); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); + CHECK(text_edit->get_caret_column() == 0); + + text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); + for (int i = 0; i < 3; i++) { + text_edit->insert_line_at(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); + } + MessageQueue::get_singleton()->flush(); + + text_edit->set_caret_blink_enabled(false); + CHECK_FALSE(text_edit->is_caret_blink_enabled()); + + text_edit->set_caret_blink_enabled(true); + CHECK(text_edit->is_caret_blink_enabled()); + + text_edit->set_caret_blink_interval(10); + CHECK(text_edit->get_caret_blink_interval() == 10); + + ERR_PRINT_OFF; + text_edit->set_caret_blink_interval(-1); + CHECK(text_edit->get_caret_blink_interval() == 10); + + text_edit->set_caret_blink_interval(0); + CHECK(text_edit->get_caret_blink_interval() == 10); + ERR_PRINT_ON; + + text_edit->set_caret_type(TextEdit::CaretType::CARET_TYPE_LINE); + CHECK(text_edit->get_caret_type() == TextEdit::CaretType::CARET_TYPE_LINE); + + text_edit->set_caret_type(TextEdit::CaretType::CARET_TYPE_BLOCK); + CHECK(text_edit->get_caret_type() == TextEdit::CaretType::CARET_TYPE_BLOCK); + + text_edit->set_caret_type(TextEdit::CaretType::CARET_TYPE_LINE); + CHECK(text_edit->get_caret_type() == TextEdit::CaretType::CARET_TYPE_LINE); + + int caret_col = text_edit->get_caret_column(); + text_edit->set_move_caret_on_right_click_enabled(false); + CHECK_FALSE(text_edit->is_move_caret_on_right_click_enabled()); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(100, 1), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE); + CHECK(text_edit->get_caret_column() == caret_col); + + text_edit->set_move_caret_on_right_click_enabled(true); + CHECK(text_edit->is_move_caret_on_right_click_enabled()); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(100, 1), MouseButton::RIGHT, MouseButton::MASK_RIGHT, Key::NONE); + CHECK(text_edit->get_caret_column() != caret_col); + + text_edit->set_move_caret_on_right_click_enabled(false); + CHECK_FALSE(text_edit->is_move_caret_on_right_click_enabled()); + + text_edit->set_caret_column(0); + CHECK(text_edit->get_word_under_caret() == "Lorem"); + + text_edit->set_caret_column(4); + CHECK(text_edit->get_word_under_caret() == "Lorem"); + + // Should this work? + text_edit->set_caret_column(5); + CHECK(text_edit->get_word_under_caret() == ""); + + text_edit->set_caret_column(6); + CHECK(text_edit->get_word_under_caret() == ""); + + text_edit->set_caret_line(1); + CHECK(text_edit->get_caret_line() == 1); + + text_edit->set_caret_line(-1); + CHECK(text_edit->get_caret_line() == 0); + text_edit->set_caret_line(100); + CHECK(text_edit->get_caret_line() == 3); + + text_edit->set_caret_column(-1); + CHECK(text_edit->get_caret_column() == 0); + text_edit->set_caret_column(10000000); + CHECK(text_edit->get_caret_column() == 141); + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] multicaret") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + text_edit->set_multiple_carets_enabled(true); + + Array empty_signal_args; + empty_signal_args.push_back(Array()); + + SIGNAL_WATCH(text_edit, "caret_changed"); + + text_edit->set_text("this is\nsome test\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SUBCASE("[TextEdit] add remove caret") { + // Overlapping + CHECK(text_edit->add_caret(0, 0) == -1); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK_FALSE("caret_changed"); + + // Selection + text_edit->select(0, 0, 2, 4); + CHECK(text_edit->add_caret(0, 0) == -1); + CHECK(text_edit->add_caret(2, 4) == -1); + CHECK(text_edit->add_caret(1, 2) == -1); + + // Out of bounds + CHECK(text_edit->add_caret(-1, 0) == -1); + CHECK(text_edit->add_caret(5, 0) == -1); + CHECK(text_edit->add_caret(0, 100) == -1); + + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK_FALSE("caret_changed"); + + CHECK(text_edit->get_caret_count() == 1); + + text_edit->deselect(); + SIGNAL_CHECK_FALSE("caret_changed"); + + CHECK(text_edit->add_caret(0, 1) == 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("caret_changed", empty_signal_args); + CHECK(text_edit->get_caret_count() == 2); + + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 1); + + ERR_PRINT_OFF; + text_edit->remove_caret(-1); + text_edit->remove_caret(5); + ERR_PRINT_ON; + CHECK(text_edit->get_caret_count() == 2); + SIGNAL_CHECK_FALSE("caret_changed"); + + text_edit->remove_caret(0); + SIGNAL_CHECK_FALSE("caret_changed"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 1); + + ERR_PRINT_OFF; + text_edit->remove_caret(0); + CHECK(text_edit->get_caret_count() == 1); + ERR_PRINT_ON; + } + + SUBCASE("[TextEdit] caret index edit order") { + Vector<int> caret_index_get_order; + caret_index_get_order.push_back(1); + caret_index_get_order.push_back(0); + + CHECK(text_edit->add_caret(1, 0)); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order); + + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(1); + CHECK(text_edit->add_caret(0, 0)); + CHECK(text_edit->get_caret_count() == 2); + + caret_index_get_order.write[0] = 0; + caret_index_get_order.write[1] = 1; + CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order); + } + + SUBCASE("[TextEdit] add caret at carets") { + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(1); + text_edit->set_caret_column(9); + + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 2); + + text_edit->add_caret_at_carets(false); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 7); + + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->select(0, 0, 0, 4); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_selection_from_line(1) == 1); + CHECK(text_edit->get_selection_to_line(1) == 1); + CHECK(text_edit->get_selection_from_column(1) == 0); + CHECK(text_edit->get_selection_to_column(1) == 3); + + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_selection_from_line(2) == 2); + CHECK(text_edit->get_selection_to_line(2) == 2); + CHECK(text_edit->get_selection_from_column(2) == 0); + CHECK(text_edit->get_selection_to_column(2) == 4); + } + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] line wrapping") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + text_edit->grab_focus(); + + // Set size for boundary. + text_edit->set_size(Size2(800, 200)); + text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); + CHECK_FALSE(text_edit->is_line_wrapped(0)); + CHECK(text_edit->get_line_wrap_count(0) == 0); + CHECK(text_edit->get_line_wrap_index_at_column(0, 130) == 0); + CHECK(text_edit->get_line_wrapped_text(0).size() == 1); + + SIGNAL_WATCH(text_edit, "text_set"); + SIGNAL_WATCH(text_edit, "text_changed"); + SIGNAL_WATCH(text_edit, "lines_edited_from"); + SIGNAL_WATCH(text_edit, "caret_changed"); + + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + SIGNAL_CHECK_FALSE("text_set"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("caret_changed"); + + CHECK(text_edit->is_line_wrapped(0)); + CHECK(text_edit->get_line_wrap_count(0) == 1); + CHECK(text_edit->get_line_wrap_index_at_column(0, 130) == 1); + CHECK(text_edit->get_line_wrapped_text(0).size() == 2); + + SIGNAL_UNWATCH(text_edit, "text_set"); + SIGNAL_UNWATCH(text_edit, "text_changed"); + SIGNAL_UNWATCH(text_edit, "lines_edited_from"); + SIGNAL_UNWATCH(text_edit, "caret_changed"); + + ERR_PRINT_OFF; + CHECK_FALSE(text_edit->is_line_wrapped(-1)); + CHECK_FALSE(text_edit->is_line_wrapped(1)); + CHECK(text_edit->get_line_wrap_count(-1) == 0); + CHECK(text_edit->get_line_wrap_count(1) == 0); + CHECK(text_edit->get_line_wrap_index_at_column(-1, 0) == 0); + CHECK(text_edit->get_line_wrap_index_at_column(0, -1) == 0); + CHECK(text_edit->get_line_wrap_index_at_column(1, 0) == 0); + CHECK(text_edit->get_line_wrap_index_at_column(0, 10000) == 0); + CHECK(text_edit->get_line_wrapped_text(-1).size() == 0); + CHECK(text_edit->get_line_wrapped_text(1).size() == 0); + ERR_PRINT_ON; + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] viewport") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + // No subcases here for performance. + text_edit->set_size(Size2(800, 600)); + for (int i = 0; i < 50; i++) { + text_edit->insert_line_at(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec varius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); + } + MessageQueue::get_singleton()->flush(); + + const int visible_lines = text_edit->get_visible_line_count(); + const int total_visible_lines = text_edit->get_total_visible_line_count(); + CHECK(total_visible_lines == 51); + + // First visible line. + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + text_edit->set_line_as_first_visible(visible_lines); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines); + CHECK(text_edit->get_v_scroll() == visible_lines); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + ERR_PRINT_OFF; + text_edit->set_line_as_first_visible(-1); + text_edit->set_line_as_first_visible(500); + text_edit->set_line_as_first_visible(0, -1); + text_edit->set_line_as_first_visible(0, 500); + CHECK(text_edit->get_first_visible_line() == visible_lines); + ERR_PRINT_ON; + + // Wrap. + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() > total_visible_lines); + + text_edit->set_line_as_first_visible(5, 1); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 5); + CHECK(text_edit->get_v_scroll() == 11); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 6); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1); + + // Reset. + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() == total_visible_lines); + text_edit->set_line_as_first_visible(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Last visible line. + text_edit->set_line_as_last_visible(visible_lines * 2); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines); + CHECK(text_edit->get_v_scroll() == visible_lines); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + ERR_PRINT_OFF; + text_edit->set_line_as_last_visible(-1); + text_edit->set_line_as_last_visible(500); + text_edit->set_line_as_last_visible(0, -1); + text_edit->set_line_as_last_visible(0, 500); + CHECK(text_edit->get_first_visible_line() == visible_lines); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + ERR_PRINT_ON; + + // Wrap. + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() > total_visible_lines); + + text_edit->set_line_as_last_visible(visible_lines + 5, 1); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 16); + CHECK(text_edit->get_v_scroll() == 32.0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines + 5); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Reset. + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() == total_visible_lines); + text_edit->set_line_as_first_visible(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Center. + text_edit->set_line_as_center_visible(visible_lines + (visible_lines / 2)); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines); + CHECK(text_edit->get_v_scroll() == visible_lines); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + ERR_PRINT_OFF; + text_edit->set_line_as_last_visible(-1); + text_edit->set_line_as_last_visible(500); + text_edit->set_line_as_last_visible(0, -1); + text_edit->set_line_as_last_visible(0, 500); + CHECK(text_edit->get_first_visible_line() == visible_lines); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + ERR_PRINT_ON; + + // Wrap. + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() > total_visible_lines); + + text_edit->set_line_as_center_visible(visible_lines + (visible_lines / 2) + 5, 1); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines + (visible_lines / 2)); + CHECK(text_edit->get_v_scroll() == (visible_lines * 3)); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1); + + // Scroll past eof. + int line_count = text_edit->get_line_count(); + text_edit->set_scroll_past_end_of_file_enabled(true); + MessageQueue::get_singleton()->flush(); + text_edit->set_line_as_center_visible(line_count - 1); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->get_first_visible_line() == (visible_lines * 2) + 3); + CHECK(text_edit->get_v_scroll() == (visible_lines * 4) + 6); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) + 8); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + text_edit->set_scroll_past_end_of_file_enabled(false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == (visible_lines * 2) + 3); + CHECK(text_edit->get_v_scroll() == (visible_lines * 4) - 4); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) + 8); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Reset. + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() == total_visible_lines); + text_edit->set_line_as_first_visible(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Auto adjust - todo: horizontal scroll. + // Below. + MessageQueue::get_singleton()->flush(); + CHECK_FALSE(text_edit->is_caret_visible()); + text_edit->set_caret_line(visible_lines + 5, false); + CHECK_FALSE(text_edit->is_caret_visible()); + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->is_caret_visible()); + CHECK(text_edit->get_first_visible_line() == 5); + CHECK(text_edit->get_v_scroll() == 5); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines - 1) + 5); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + text_edit->center_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines - 5); + CHECK(text_edit->get_v_scroll() == visible_lines - 5); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 6); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Caret visible, do nothing. + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines - 5); + CHECK(text_edit->get_v_scroll() == visible_lines - 5); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 6); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Above. + text_edit->set_caret_line(1, false); + MessageQueue::get_singleton()->flush(); + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->is_caret_visible()); + CHECK(text_edit->get_first_visible_line() == 1); + CHECK(text_edit->get_v_scroll() == 1); + CHECK(text_edit->get_last_full_visible_line() == visible_lines); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + text_edit->set_line_as_first_visible(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Wrap + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() > total_visible_lines); + + text_edit->set_caret_line(visible_lines + 5, false, true, 1); + MessageQueue::get_singleton()->flush(); + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + + CHECK(text_edit->get_first_visible_line() == (visible_lines / 2) + 4); + CHECK(text_edit->get_v_scroll() == (visible_lines + (visible_lines / 2)) - 1); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines) + 3); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1); + CHECK(text_edit->get_caret_wrap_index() == 1); + + text_edit->center_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines); + CHECK(text_edit->get_v_scroll() == (visible_lines * 2) + 1); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 11); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1); + + // Caret visible, do nothing. + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == visible_lines); + CHECK(text_edit->get_v_scroll() == (visible_lines * 2) + 1); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 11); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1); + + // Above. + text_edit->set_caret_line(1, false, true, 1); + MessageQueue::get_singleton()->flush(); + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->is_caret_visible()); + CHECK(text_edit->get_first_visible_line() == 1); + CHECK(text_edit->get_v_scroll() == 3); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines / 2) + 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 1); + CHECK(text_edit->get_caret_wrap_index() == 1); + + text_edit->set_line_as_first_visible(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->is_caret_visible()); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 11); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + text_edit->adjust_viewport_to_caret(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 11); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + // Reset. + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_total_visible_line_count() == total_visible_lines); + text_edit->set_line_as_first_visible(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + // Smooth scroll. + text_edit->set_v_scroll_speed(10); + CHECK(text_edit->get_v_scroll_speed() == 10); + ERR_PRINT_OFF; + text_edit->set_v_scroll_speed(-1); + CHECK(text_edit->get_v_scroll_speed() == 10); + + text_edit->set_v_scroll_speed(0); + CHECK(text_edit->get_v_scroll_speed() == 10); + + text_edit->set_v_scroll_speed(1); + CHECK(text_edit->get_v_scroll_speed() == 1); + ERR_PRINT_ON; + + // Scroll. + int v_scroll = text_edit->get_v_scroll(); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_DOWN, MouseButton::WHEEL_DOWN, Key::NONE); + CHECK(text_edit->get_v_scroll() > v_scroll); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_UP, MouseButton::WHEEL_UP, Key::NONE); + CHECK(text_edit->get_v_scroll() == v_scroll); + + // smooth scroll speed. + text_edit->set_smooth_scroll_enabled(true); + + v_scroll = text_edit->get_v_scroll(); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_DOWN, MouseButton::WHEEL_DOWN, Key::NONE); + text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS); + CHECK(text_edit->get_v_scroll() >= v_scroll); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_UP, MouseButton::WHEEL_UP, Key::NONE); + text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS); + CHECK(text_edit->get_v_scroll() == v_scroll); + + v_scroll = text_edit->get_v_scroll(); + text_edit->set_v_scroll_speed(10000); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_DOWN, MouseButton::WHEEL_DOWN, Key::NONE); + text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS); + CHECK(text_edit->get_v_scroll() >= v_scroll); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit, Point2i(10, 10), MouseButton::WHEEL_UP, MouseButton::WHEEL_UP, Key::NONE); + text_edit->notification(TextEdit::NOTIFICATION_INTERNAL_PHYSICS_PROCESS); + CHECK(text_edit->get_v_scroll() == v_scroll); + + ERR_PRINT_OFF; + CHECK(text_edit->get_scroll_pos_for_line(-1) == 0); + CHECK(text_edit->get_scroll_pos_for_line(1000) == 0); + CHECK(text_edit->get_scroll_pos_for_line(1, -1) == 0); + CHECK(text_edit->get_scroll_pos_for_line(1, 100) == 0); + ERR_PRINT_ON; + + text_edit->set_h_scroll(-100); + CHECK(text_edit->get_h_scroll() == 0); + + text_edit->set_h_scroll(10000000); + CHECK(text_edit->get_h_scroll() == 313); + + text_edit->set_h_scroll(-100); + CHECK(text_edit->get_h_scroll() == 0); + + text_edit->set_smooth_scroll_enabled(false); + + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + + text_edit->grab_focus(); + SEND_GUI_ACTION(text_edit, "ui_text_scroll_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_first_visible_line() == 1); + CHECK(text_edit->get_v_scroll() == 1); + CHECK(text_edit->get_last_full_visible_line() == visible_lines); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_scroll_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + // Page down, similar to VSCode, to end of page then scroll. + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 21); + CHECK(text_edit->get_first_visible_line() == 0); + CHECK(text_edit->get_v_scroll() == 0); + CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 41); + CHECK(text_edit->get_first_visible_line() == 20); + CHECK(text_edit->get_v_scroll() == 20); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines - 1) * 2); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 21); + CHECK(text_edit->get_first_visible_line() == 20); + CHECK(text_edit->get_v_scroll() == 20); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines - 1) * 2); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_first_visible_line() == 1); + CHECK(text_edit->get_v_scroll() == 1); + CHECK(text_edit->get_last_full_visible_line() == visible_lines); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_NONE); + MessageQueue::get_singleton()->flush(); + + text_edit->grab_focus(); + SEND_GUI_ACTION(text_edit, "ui_text_scroll_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_first_visible_line() == 2); + CHECK(text_edit->get_v_scroll() == 2); + CHECK(text_edit->get_last_full_visible_line() == visible_lines + 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_scroll_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_first_visible_line() == 1); + CHECK(text_edit->get_v_scroll() == 1); + CHECK(text_edit->get_last_full_visible_line() == visible_lines); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + // Page down, similar to VSCode, to end of page then scroll. + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 22); + CHECK(text_edit->get_first_visible_line() == 1); + CHECK(text_edit->get_v_scroll() == 1); + CHECK(text_edit->get_last_full_visible_line() == visible_lines); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_down"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 42); + CHECK(text_edit->get_first_visible_line() == 21); + CHECK(text_edit->get_v_scroll() == 21); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 22); + CHECK(text_edit->get_first_visible_line() == 21); + CHECK(text_edit->get_v_scroll() == 21); + CHECK(text_edit->get_last_full_visible_line() == (visible_lines * 2) - 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + SEND_GUI_ACTION(text_edit, "ui_text_caret_page_up"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_first_visible_line() == 2); + CHECK(text_edit->get_v_scroll() == 2); + CHECK(text_edit->get_last_full_visible_line() == visible_lines + 1); + CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); + CHECK(text_edit->get_caret_wrap_index() == 0); + + // Typing and undo / redo should adjust viewport + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->set_line_as_first_visible(5); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 5); + + SEND_GUI_KEY_EVENT(text_edit, Key::A); + CHECK(text_edit->get_first_visible_line() == 0); + + text_edit->set_line_as_first_visible(5); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 5); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + + text_edit->set_line_as_first_visible(5); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 5); + + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_first_visible_line() == 0); + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] setter getters") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + SUBCASE("[TextEdit] set and get placeholder") { + text_edit->set_placeholder("test\nplaceholder"); + CHECK(text_edit->get_placeholder() == "test\nplaceholder"); + + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_line_count() == 1); + CHECK(text_edit->get_last_full_visible_line() == 0); + } + + SUBCASE("[TextEdit] highlight current line") { + text_edit->set_highlight_current_line(true); + CHECK(text_edit->is_highlight_current_line_enabled()); + text_edit->set_highlight_current_line(false); + CHECK_FALSE(text_edit->is_highlight_current_line_enabled()); + } + + SUBCASE("[TextEdit] highlight all occurrences") { + text_edit->set_highlight_all_occurrences(true); + CHECK(text_edit->is_highlight_all_occurrences_enabled()); + text_edit->set_highlight_all_occurrences(false); + CHECK_FALSE(text_edit->is_highlight_all_occurrences_enabled()); + } + + SUBCASE("[TextEdit] draw control chars") { + text_edit->set_draw_control_chars(true); + CHECK(text_edit->get_draw_control_chars()); + text_edit->set_draw_control_chars(false); + CHECK_FALSE(text_edit->get_draw_control_chars()); + } + + SUBCASE("[TextEdit] draw tabs") { + text_edit->set_draw_tabs(true); + CHECK(text_edit->is_drawing_tabs()); + text_edit->set_draw_tabs(false); + CHECK_FALSE(text_edit->is_drawing_tabs()); + } + + SUBCASE("[TextEdit] draw spaces") { + text_edit->set_draw_spaces(true); + CHECK(text_edit->is_drawing_spaces()); + text_edit->set_draw_spaces(false); + CHECK_FALSE(text_edit->is_drawing_spaces()); + } + + SUBCASE("[TextEdit] draw minimao") { + text_edit->set_draw_minimap(true); + CHECK(text_edit->is_drawing_minimap()); + text_edit->set_draw_minimap(false); + CHECK_FALSE(text_edit->is_drawing_minimap()); + } + + SUBCASE("[TextEdit] minimap width") { + text_edit->set_minimap_width(-1); + CHECK(text_edit->get_minimap_width() == -1); + text_edit->set_minimap_width(1000); + CHECK(text_edit->get_minimap_width() == 1000); + } + + SUBCASE("[TextEdit] line color background") { + ERR_PRINT_OFF; + text_edit->set_line_background_color(-1, Color("#ff0000")); + text_edit->set_line_background_color(0, Color("#00ff00")); + text_edit->set_line_background_color(1, Color("#0000ff")); + + CHECK(text_edit->get_line_background_color(-1) == Color()); + CHECK(text_edit->get_line_background_color(0) == Color("#00ff00")); + CHECK(text_edit->get_line_background_color(1) == Color()); + ERR_PRINT_ON; + + text_edit->set_line_background_color(0, Color("#ffff00")); + CHECK(text_edit->get_line_background_color(0) == Color("#ffff00")); + } + + memdelete(text_edit); +} + +TEST_CASE("[SceneTree][TextEdit] gutters") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + + Array empty_signal_args; + empty_signal_args.push_back(Array()); + + SIGNAL_WATCH(text_edit, "gutter_clicked"); + SIGNAL_WATCH(text_edit, "gutter_added"); + SIGNAL_WATCH(text_edit, "gutter_removed"); + + SUBCASE("[TextEdit] gutter add and remove") { + text_edit->add_gutter(); + CHECK(text_edit->get_gutter_count() == 1); + CHECK(text_edit->get_gutter_width(0) == 24); + CHECK(text_edit->get_total_gutter_width() == 24 + 2); + SIGNAL_CHECK("gutter_added", empty_signal_args); + + text_edit->set_gutter_name(0, "test_gutter"); + CHECK(text_edit->get_gutter_name(0) == "test_gutter"); + + text_edit->set_gutter_width(0, 10); + CHECK(text_edit->get_gutter_width(0) == 10); + CHECK(text_edit->get_total_gutter_width() == 10 + 2); + + text_edit->add_gutter(-100); + text_edit->set_gutter_width(1, 10); + CHECK(text_edit->get_gutter_width(1) == 10); + CHECK(text_edit->get_total_gutter_width() == 20 + 2); + CHECK(text_edit->get_gutter_count() == 2); + CHECK(text_edit->get_gutter_name(0) == "test_gutter"); + SIGNAL_CHECK("gutter_added", empty_signal_args); + + text_edit->set_gutter_draw(1, false); + CHECK(text_edit->get_total_gutter_width() == 10 + 2); + + text_edit->add_gutter(100); + CHECK(text_edit->get_gutter_count() == 3); + CHECK(text_edit->get_gutter_width(2) == 24); + CHECK(text_edit->get_total_gutter_width() == 34 + 2); + CHECK(text_edit->get_gutter_name(0) == "test_gutter"); + SIGNAL_CHECK("gutter_added", empty_signal_args); + + text_edit->add_gutter(0); + CHECK(text_edit->get_gutter_count() == 4); + CHECK(text_edit->get_gutter_width(0) == 24); + CHECK(text_edit->get_total_gutter_width() == 58 + 2); + CHECK(text_edit->get_gutter_name(1) == "test_gutter"); + SIGNAL_CHECK("gutter_added", empty_signal_args); + + text_edit->remove_gutter(2); + CHECK(text_edit->get_gutter_name(1) == "test_gutter"); + CHECK(text_edit->get_gutter_count() == 3); + CHECK(text_edit->get_total_gutter_width() == 58 + 2); + SIGNAL_CHECK("gutter_removed", empty_signal_args); + + text_edit->remove_gutter(0); + CHECK(text_edit->get_gutter_name(0) == "test_gutter"); + CHECK(text_edit->get_gutter_count() == 2); + CHECK(text_edit->get_total_gutter_width() == 34 + 2); + SIGNAL_CHECK("gutter_removed", empty_signal_args); + + ERR_PRINT_OFF; + text_edit->remove_gutter(-1); + SIGNAL_CHECK_FALSE("gutter_removed"); + + text_edit->remove_gutter(100); + SIGNAL_CHECK_FALSE("gutter_removed"); + + CHECK(text_edit->get_gutter_name(-1) == ""); + CHECK(text_edit->get_gutter_name(100) == ""); + ERR_PRINT_ON; + } + + SUBCASE("[TextEdit] gutter data") { + text_edit->add_gutter(); + CHECK(text_edit->get_gutter_count() == 1); + SIGNAL_CHECK("gutter_added", empty_signal_args); + + text_edit->set_gutter_name(0, "test_gutter"); + CHECK(text_edit->get_gutter_name(0) == "test_gutter"); + + text_edit->set_gutter_width(0, 10); + CHECK(text_edit->get_gutter_width(0) == 10); + + text_edit->set_gutter_clickable(0, true); + CHECK(text_edit->is_gutter_clickable(0)); + + text_edit->set_gutter_overwritable(0, true); + CHECK(text_edit->is_gutter_overwritable(0)); + + text_edit->set_gutter_type(0, TextEdit::GutterType::GUTTER_TYPE_CUSTOM); + CHECK(text_edit->get_gutter_type(0) == TextEdit::GutterType::GUTTER_TYPE_CUSTOM); + + text_edit->set_text("test\ntext"); + + ERR_PRINT_OFF; + text_edit->set_line_gutter_metadata(1, 0, "test"); + text_edit->set_line_gutter_metadata(0, -1, "test"); + text_edit->set_line_gutter_metadata(0, 2, "test"); + text_edit->set_line_gutter_metadata(2, 0, "test"); + text_edit->set_line_gutter_metadata(-1, 0, "test"); + + CHECK(text_edit->get_line_gutter_metadata(1, 0) == "test"); + CHECK(text_edit->get_line_gutter_metadata(0, -1) == ""); + CHECK(text_edit->get_line_gutter_metadata(0, 2) == ""); + CHECK(text_edit->get_line_gutter_metadata(2, 0) == ""); + CHECK(text_edit->get_line_gutter_metadata(-1, 0) == ""); + + text_edit->set_line_gutter_text(1, 0, "test"); + text_edit->set_line_gutter_text(0, -1, "test"); + text_edit->set_line_gutter_text(0, 2, "test"); + text_edit->set_line_gutter_text(2, 0, "test"); + text_edit->set_line_gutter_text(-1, 0, "test"); + + CHECK(text_edit->get_line_gutter_text(1, 0) == "test"); + CHECK(text_edit->get_line_gutter_text(0, -1) == ""); + CHECK(text_edit->get_line_gutter_text(0, 2) == ""); + CHECK(text_edit->get_line_gutter_text(2, 0) == ""); + CHECK(text_edit->get_line_gutter_text(-1, 0) == ""); + + text_edit->set_line_gutter_item_color(1, 0, Color(1, 0, 0)); + text_edit->set_line_gutter_item_color(0, -1, Color(1, 0, 0)); + text_edit->set_line_gutter_item_color(0, 2, Color(1, 0, 0)); + text_edit->set_line_gutter_item_color(2, 0, Color(1, 0, 0)); + text_edit->set_line_gutter_item_color(-1, 0, Color(1, 0, 0)); + + CHECK(text_edit->get_line_gutter_item_color(1, 0) == Color(1, 0, 0)); + CHECK(text_edit->get_line_gutter_item_color(0, -1) == Color()); + CHECK(text_edit->get_line_gutter_item_color(0, 2) == Color()); + CHECK(text_edit->get_line_gutter_item_color(2, 0) == Color()); + CHECK(text_edit->get_line_gutter_item_color(-1, 0) == Color()); + + text_edit->set_line_gutter_clickable(1, 0, true); + text_edit->set_line_gutter_clickable(0, -1, true); + text_edit->set_line_gutter_clickable(0, 2, true); + text_edit->set_line_gutter_clickable(2, 0, true); + text_edit->set_line_gutter_clickable(-1, 0, true); + + CHECK(text_edit->is_line_gutter_clickable(1, 0) == true); + CHECK(text_edit->is_line_gutter_clickable(0, -1) == false); + CHECK(text_edit->is_line_gutter_clickable(0, 2) == false); + CHECK(text_edit->is_line_gutter_clickable(2, 0) == false); + CHECK(text_edit->is_line_gutter_clickable(-1, 0) == false); + ERR_PRINT_ON; + + // Merging tested via CodeEdit gutters. + } + + SIGNAL_UNWATCH(text_edit, "gutter_clicked"); + SIGNAL_UNWATCH(text_edit, "gutter_added"); + SIGNAL_UNWATCH(text_edit, "gutter_removed"); + memdelete(text_edit); +} + +} // namespace TestTextEdit + +#endif // TEST_TEXT_EDIT_H diff --git a/tests/scene/test_theme.h b/tests/scene/test_theme.h new file mode 100644 index 0000000000..f5b21eec32 --- /dev/null +++ b/tests/scene/test_theme.h @@ -0,0 +1,271 @@ +/*************************************************************************/ +/* test_theme.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_THEME_H +#define TEST_THEME_H + +#include "scene/resources/theme.h" +#include "tests/test_tools.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestTheme { + +class Fixture { +public: + struct DataEntry { + Theme::DataType type; + Variant value; + } const valid_data[Theme::DATA_TYPE_MAX] = { + { Theme::DATA_TYPE_COLOR, Color() }, + { Theme::DATA_TYPE_CONSTANT, 42 }, + { Theme::DATA_TYPE_FONT, Ref<FontFile>(memnew(FontFile)) }, + { Theme::DATA_TYPE_FONT_SIZE, 42 }, + { Theme::DATA_TYPE_ICON, Ref<Texture>(memnew(ImageTexture)) }, + { Theme::DATA_TYPE_STYLEBOX, Ref<StyleBox>(memnew(StyleBoxFlat)) }, + }; + + const StringName valid_item_name = "valid_item_name"; + const StringName valid_type_name = "ValidTypeName"; +}; + +TEST_CASE_FIXTURE(Fixture, "[Theme] Good theme type names") { + StringName names[] = { + "", // Empty name. + "CapitalizedName", + "snake_cased_name", + "42", + "_Underscore_", + }; + + SUBCASE("add_type") { + for (const StringName &name : names) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->add_type(name); + CHECK_FALSE(ed.has_error); + } + } + + SUBCASE("set_theme_item") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_theme_item(entry.type, valid_item_name, name, entry.value); + CHECK_FALSE(ed.has_error); + } + } + } + + SUBCASE("add_theme_item_type") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->add_theme_item_type(entry.type, name); + CHECK_FALSE(ed.has_error); + } + } + } + + SUBCASE("set_type_variation") { + for (const StringName &name : names) { + if (name == StringName()) { // Skip empty here, not allowed. + continue; + } + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_type_variation(valid_type_name, name); + CHECK_FALSE(ed.has_error); + } + for (const StringName &name : names) { + if (name == StringName()) { // Skip empty here, not allowed. + continue; + } + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_type_variation(name, valid_type_name); + CHECK_FALSE(ed.has_error); + } + } +} + +TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme type names") { + StringName names[] = { + "With/Slash", + "With Space", + "With@various$symbols!", + String::utf8("contains_汉字"), + }; + + ERR_PRINT_OFF; // All these rightfully print errors. + + SUBCASE("add_type") { + for (const StringName &name : names) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->add_type(name); + CHECK(ed.has_error); + } + } + + SUBCASE("set_theme_item") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_theme_item(entry.type, valid_item_name, name, entry.value); + CHECK(ed.has_error); + } + } + } + + SUBCASE("add_theme_item_type") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->add_theme_item_type(entry.type, name); + CHECK(ed.has_error); + } + } + } + + SUBCASE("set_type_variation") { + for (const StringName &name : names) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_type_variation(valid_type_name, name); + CHECK(ed.has_error); + } + for (const StringName &name : names) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_type_variation(name, valid_type_name); + CHECK(ed.has_error); + } + } + + ERR_PRINT_ON; +} + +TEST_CASE_FIXTURE(Fixture, "[Theme] Good theme item names") { + StringName names[] = { + "CapitalizedName", + "snake_cased_name", + "42", + "_Underscore_", + }; + + SUBCASE("set_theme_item") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_theme_item(entry.type, name, valid_type_name, entry.value); + CHECK_FALSE(ed.has_error); + CHECK(theme->has_theme_item(entry.type, name, valid_type_name)); + } + } + } + + SUBCASE("rename_theme_item") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + theme->set_theme_item(entry.type, valid_item_name, valid_type_name, entry.value); + + ErrorDetector ed; + theme->rename_theme_item(entry.type, valid_item_name, name, valid_type_name); + CHECK_FALSE(ed.has_error); + CHECK_FALSE(theme->has_theme_item(entry.type, valid_item_name, valid_type_name)); + CHECK(theme->has_theme_item(entry.type, name, valid_type_name)); + } + } + } +} + +TEST_CASE_FIXTURE(Fixture, "[Theme] Bad theme item names") { + StringName names[] = { + "", // Empty name. + "With/Slash", + "With Space", + "With@various$symbols!", + String::utf8("contains_汉字"), + }; + + ERR_PRINT_OFF; // All these rightfully print errors. + + SUBCASE("set_theme_item") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + + ErrorDetector ed; + theme->set_theme_item(entry.type, name, valid_type_name, entry.value); + CHECK(ed.has_error); + CHECK_FALSE(theme->has_theme_item(entry.type, name, valid_type_name)); + } + } + } + + SUBCASE("rename_theme_item") { + for (const StringName &name : names) { + for (const DataEntry &entry : valid_data) { + Ref<Theme> theme = memnew(Theme); + theme->set_theme_item(entry.type, valid_item_name, valid_type_name, entry.value); + + ErrorDetector ed; + theme->rename_theme_item(entry.type, valid_item_name, name, valid_type_name); + CHECK(ed.has_error); + CHECK(theme->has_theme_item(entry.type, valid_item_name, valid_type_name)); + CHECK_FALSE(theme->has_theme_item(entry.type, name, valid_type_name)); + } + } + } + + ERR_PRINT_ON; +} + +} // namespace TestTheme + +#endif // TEST_THEME_H |