basalt-gtk 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runLinux.js +29 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +362 -0
  5. package/native/gtk/ColorSchemeGtk.cpp +58 -0
  6. package/native/gtk/ComponentRegistryGtk.cpp +101 -0
  7. package/native/gtk/FontRegistryFontconfig.cpp +127 -0
  8. package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
  9. package/native/gtk/GtkAnimationChoreographer.h +49 -0
  10. package/native/gtk/GtkDropTarget.cpp +269 -0
  11. package/native/gtk/GtkDropTarget.h +40 -0
  12. package/native/gtk/GtkFocus.cpp +296 -0
  13. package/native/gtk/GtkFocus.h +113 -0
  14. package/native/gtk/GtkImageLoader.cpp +128 -0
  15. package/native/gtk/GtkImageLoader.h +87 -0
  16. package/native/gtk/GtkKeyEvents.cpp +91 -0
  17. package/native/gtk/GtkKeyEvents.h +21 -0
  18. package/native/gtk/GtkMenuBar.cpp +45 -0
  19. package/native/gtk/GtkMountingManager.cpp +1027 -0
  20. package/native/gtk/GtkMountingManager.h +146 -0
  21. package/native/gtk/GtkRunLoopObserver.cpp +77 -0
  22. package/native/gtk/GtkRunLoopObserver.h +41 -0
  23. package/native/gtk/GtkScrollView.cpp +626 -0
  24. package/native/gtk/GtkScrollView.h +209 -0
  25. package/native/gtk/GtkSkiaContext.cpp +248 -0
  26. package/native/gtk/GtkSkiaContext.h +62 -0
  27. package/native/gtk/GtkSkiaModule.cpp +80 -0
  28. package/native/gtk/GtkSkiaModule.h +51 -0
  29. package/native/gtk/GtkSkiaPeer.cpp +285 -0
  30. package/native/gtk/GtkSkiaPeer.h +63 -0
  31. package/native/gtk/GtkTextInput.cpp +428 -0
  32. package/native/gtk/GtkTextInput.h +123 -0
  33. package/native/gtk/GtkTextPeer.cpp +425 -0
  34. package/native/gtk/GtkTextPeer.h +87 -0
  35. package/native/gtk/GtkTitleBar.cpp +273 -0
  36. package/native/gtk/GtkTitleBar.h +96 -0
  37. package/native/gtk/GtkTitleBarLayout.cpp +45 -0
  38. package/native/gtk/GtkTitleBarLayout.h +36 -0
  39. package/native/gtk/GtkTouchDispatcher.cpp +671 -0
  40. package/native/gtk/GtkTouchDispatcher.h +139 -0
  41. package/native/gtk/GtkWindowControl.cpp +223 -0
  42. package/native/gtk/GtkWindowModule.cpp +194 -0
  43. package/native/gtk/GtkWindowModule.h +62 -0
  44. package/native/gtk/PangoTextLayout.cpp +295 -0
  45. package/native/gtk/PangoTextLayout.h +54 -0
  46. package/native/gtk/PangoTextLayoutManager.cpp +140 -0
  47. package/native/gtk/PlatformServicesGtk.cpp +583 -0
  48. package/native/gtk/RnView.cpp +1407 -0
  49. package/native/gtk/RnView.h +339 -0
  50. package/native/gtk/demo_layout_gtk.cpp +57 -0
  51. package/native/gtk/main_gtk.cpp +1981 -0
  52. package/native/gtk/mount_harness_gtk.cpp +143 -0
  53. package/native/tests/TestMainGtk.cpp +25 -0
  54. package/native/tests/notification_stub.cpp +103 -0
  55. package/native/tests/test_accessibility.cpp +251 -0
  56. package/native/tests/test_focus.cpp +194 -0
  57. package/native/tests/test_hittest.cpp +242 -0
  58. package/native/tests/test_image.cpp +135 -0
  59. package/native/tests/test_key_events_gtk.cpp +112 -0
  60. package/native/tests/test_mounting.cpp +415 -0
  61. package/native/tests/test_scrollview.cpp +258 -0
  62. package/native/tests/test_skia_texture.cpp +120 -0
  63. package/native/tests/test_text.cpp +147 -0
  64. package/native/tests/test_textinput.cpp +598 -0
  65. package/native/tests/test_titlebar.cpp +137 -0
  66. package/native/tests/test_titlebar_drag.cpp +138 -0
  67. package/native/tests/test_version.cpp +49 -0
  68. package/native/tests/test_view.cpp +162 -0
  69. package/native/tests/test_viewprops.cpp +254 -0
  70. package/package.json +26 -0
  71. package/react-native.config.js +21 -0
@@ -0,0 +1,143 @@
1
+ // Drives real ShadowViewMutations through the real GtkMountingManager into
2
+ // real GTK widgets, with no JS runtime, no Hermes and no Metro.
3
+ //
4
+ // Note that executeMount only *queues*: in a real host it is called on the JS
5
+ // thread and marshals to the GTK main thread. Calling it from the main thread
6
+ // here takes the same path, one main-loop turn later.
7
+ //
8
+ // This is the checkpoint between "the mounting manager compiles" and "React
9
+ // Native runs": it exercises the actual mutation walk against the actual
10
+ // Fabric types, so a mistake in the Create/Insert/Update/Remove/Delete
11
+ // handling shows up on screen rather than in review.
12
+
13
+ #include "GtkMountingManager.h"
14
+
15
+ #include <react/renderer/components/view/ViewProps.h>
16
+ #include <react/renderer/core/LayoutMetrics.h>
17
+ #include <react/renderer/graphics/Color.h>
18
+ #include <react/renderer/mounting/MountingTransaction.h>
19
+ #include <react/renderer/mounting/ShadowViewMutation.h>
20
+
21
+ #include <memory>
22
+ #include <utility>
23
+
24
+ using facebook::react::ColorComponents;
25
+ using facebook::react::LayoutMetrics;
26
+ using facebook::react::MountingTransaction;
27
+ using facebook::react::ShadowView;
28
+ using facebook::react::ShadowViewMutation;
29
+ using facebook::react::ShadowViewMutationList;
30
+ using facebook::react::SurfaceId;
31
+ using facebook::react::Tag;
32
+ using facebook::react::TransactionTelemetry;
33
+ using facebook::react::ViewProps;
34
+
35
+ namespace {
36
+
37
+ constexpr SurfaceId kSurfaceId = 1;
38
+
39
+ ShadowView makeShadowView(Tag tag,
40
+ float x, float y, float width, float height,
41
+ float red, float green, float blue,
42
+ float opacity = 1.0f) {
43
+ auto props = std::make_shared<ViewProps>();
44
+ props->backgroundColor = facebook::react::colorFromComponents(
45
+ ColorComponents{.red = red, .green = green, .blue = blue, .alpha = 1.0f});
46
+ props->opacity = opacity;
47
+
48
+ LayoutMetrics layoutMetrics;
49
+ layoutMetrics.frame = {.origin = {.x = x, .y = y},
50
+ .size = {.width = width, .height = height}};
51
+
52
+ ShadowView shadowView;
53
+ shadowView.componentName = "View";
54
+ shadowView.surfaceId = kSurfaceId;
55
+ shadowView.tag = tag;
56
+ shadowView.props = props;
57
+ shadowView.layoutMetrics = layoutMetrics;
58
+ return shadowView;
59
+ }
60
+
61
+ MountingTransaction makeTransaction(MountingTransaction::Number number,
62
+ ShadowViewMutationList &&mutations) {
63
+ return MountingTransaction(kSurfaceId, number, std::move(mutations), TransactionTelemetry{});
64
+ }
65
+
66
+ basalt::GtkMountingManager *g_mountingManager = nullptr;
67
+
68
+ // Second transaction: recolour one view, and remove another entirely. Fabric
69
+ // always emits Remove before Delete, and this reproduces that ordering.
70
+ gboolean applySecondTransaction(gpointer /*user_data*/) {
71
+ g_message("--- transaction 2: update tag 2, remove+delete tag 3 ---");
72
+
73
+ ShadowViewMutationList mutations;
74
+
75
+ // Update: same tag, new props. Recolour blue -> purple and shrink.
76
+ const ShadowView oldBlue = makeShadowView(2, 32, 32, 240, 160, 0.30f, 0.55f, 0.95f);
77
+ const ShadowView newBlue = makeShadowView(2, 32, 32, 240, 100, 0.60f, 0.35f, 0.90f);
78
+ mutations.push_back(ShadowViewMutation::UpdateMutation(oldBlue, newBlue, kSurfaceId));
79
+
80
+ // Remove then Delete, the order Fabric guarantees.
81
+ const ShadowView orange = makeShadowView(3, 296, 32, 240, 160, 0.95f, 0.45f, 0.35f);
82
+ mutations.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, orange, 1));
83
+ mutations.push_back(ShadowViewMutation::DeleteMutation(orange));
84
+
85
+ g_mountingManager->executeMount(kSurfaceId, makeTransaction(2, std::move(mutations)));
86
+
87
+ g_message("--- transaction 2 queued ---");
88
+ return G_SOURCE_REMOVE;
89
+ }
90
+
91
+ void onActivate(GtkApplication *app, gpointer /*user_data*/) {
92
+ GtkWidget *window = gtk_application_window_new(app);
93
+ gtk_window_set_title(GTK_WINDOW(window), "basalt-core — mount harness");
94
+ gtk_window_set_default_size(GTK_WINDOW(window), 640, 420);
95
+
96
+ // The host owns the root: Fabric never emits a Create for it.
97
+ RnView *root = g_mountingManager->createSurfaceRoot(kSurfaceId);
98
+ gtk_window_set_child(GTK_WINDOW(window), GTK_WIDGET(root));
99
+
100
+ g_message("--- transaction 1: create + insert 4 views ---");
101
+
102
+ ShadowViewMutationList mutations;
103
+
104
+ const ShadowView blue = makeShadowView(2, 32, 32, 240, 160, 0.30f, 0.55f, 0.95f);
105
+ const ShadowView orange = makeShadowView(3, 296, 32, 240, 160, 0.95f, 0.45f, 0.35f);
106
+ const ShadowView green = makeShadowView(4, 32, 224, 504, 140, 0.35f, 0.80f, 0.55f);
107
+ const ShadowView nested = makeShadowView(5, 24, 24, 120, 90, 1.0f, 1.0f, 1.0f, 0.85f);
108
+
109
+ // Fabric emits every Create before the Inserts that place them.
110
+ mutations.push_back(ShadowViewMutation::CreateMutation(blue));
111
+ mutations.push_back(ShadowViewMutation::CreateMutation(orange));
112
+ mutations.push_back(ShadowViewMutation::CreateMutation(green));
113
+ mutations.push_back(ShadowViewMutation::CreateMutation(nested));
114
+
115
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, blue, 0));
116
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, orange, 1));
117
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, green, 2));
118
+ mutations.push_back(ShadowViewMutation::InsertMutation(blue.tag, nested, 0));
119
+
120
+ g_mountingManager->executeMount(kSurfaceId, makeTransaction(1, std::move(mutations)));
121
+
122
+ g_message("--- transaction 1 queued (applies on the next main-loop turn) ---");
123
+
124
+ gtk_window_present(GTK_WINDOW(window));
125
+
126
+ g_timeout_add(2000, applySecondTransaction, nullptr);
127
+ }
128
+
129
+ } // namespace
130
+
131
+ int main(int argc, char **argv) {
132
+ // Constructed on the main thread: GtkMountingManager records this thread and
133
+ // asserts every executeMount arrives on it.
134
+ basalt::GtkMountingManager mountingManager;
135
+ g_mountingManager = &mountingManager;
136
+
137
+ GtkApplication *app =
138
+ gtk_application_new("dev.basalt.mountharness", G_APPLICATION_DEFAULT_FLAGS);
139
+ g_signal_connect(app, "activate", G_CALLBACK(onActivate), nullptr);
140
+ const int status = g_application_run(G_APPLICATION(app), argc, argv);
141
+ g_object_unref(app);
142
+ return status;
143
+ }
@@ -0,0 +1,25 @@
1
+ // The GTK test suite's entry point.
2
+ //
3
+ // Separate from the runner because it is the only part of the suite that knows
4
+ // about a toolkit: every test here touches widgets, and a GtkWidget cannot be
5
+ // constructed before GTK is initialised. The macOS suite has its own.
6
+
7
+ #include "TestHarness.h"
8
+
9
+ #include <gtk/gtk.h>
10
+
11
+ #include <iostream>
12
+
13
+ int main(int argc, char **argv) {
14
+
15
+ // No window is ever presented, but GTK still needs a display connection: on a
16
+ // headless machine run this under a nested or virtual display server. See
17
+ // docs/TESTING.md.
18
+ if (gtk_init_check() == FALSE) {
19
+ std::cerr << "could not initialise GTK; these tests need a display.\n"
20
+ << "On a headless Linux box: xvfb-run -a ./build/basalt_gtk_tests\n";
21
+ return 77; // The convention automake uses for "skipped", not "failed".
22
+ }
23
+
24
+ return basalt::testing::runAllTests(argc, argv);
25
+ }
@@ -0,0 +1,103 @@
1
+ // A stand-in for a desktop's notification daemon.
2
+ //
3
+ // The one thing missing from testing notifications: `GtkNotifications.cpp`
4
+ // sends `Notify` over the session bus, and neither a Mac nor a CI runner has
5
+ // anything listening for it. Without something here the only paths that could
6
+ // be exercised were the two that report why nothing can be sent.
7
+ //
8
+ // It owns `org.freedesktop.Notifications` on whatever bus it is started on,
9
+ // answers `Notify` with an increasing id, and prints what it was asked to show
10
+ // so the end-to-end suite can assert on it. Everything a real daemon does
11
+ // beyond that -- the window, the timeout, the actions, the signals back -- is
12
+ // what a test does not need and what would make this a second implementation
13
+ // rather than a fixture.
14
+ //
15
+ // Started by scripts/integration_test.py, which also starts the bus. See
16
+ // docs/TESTING.md.
17
+ #include <gio/gio.h>
18
+ #include <stdio.h>
19
+
20
+ static const char *kXml =
21
+ "<node><interface name='org.freedesktop.Notifications'>"
22
+ " <method name='Notify'>"
23
+ " <arg type='s' name='app_name' direction='in'/>"
24
+ " <arg type='u' name='replaces_id' direction='in'/>"
25
+ " <arg type='s' name='app_icon' direction='in'/>"
26
+ " <arg type='s' name='summary' direction='in'/>"
27
+ " <arg type='s' name='body' direction='in'/>"
28
+ " <arg type='as' name='actions' direction='in'/>"
29
+ " <arg type='a{sv}' name='hints' direction='in'/>"
30
+ " <arg type='i' name='timeout' direction='in'/>"
31
+ " <arg type='u' name='id' direction='out'/>"
32
+ " </method>"
33
+ " <method name='CloseNotification'>"
34
+ " <arg type='u' name='id' direction='in'/>"
35
+ " </method>"
36
+ " <method name='GetCapabilities'>"
37
+ " <arg type='as' name='caps' direction='out'/>"
38
+ " </method>"
39
+ "</interface></node>";
40
+
41
+ static guint32 nextId = 1;
42
+
43
+ static void onCall(GDBusConnection *connection, const gchar *sender, const gchar *path,
44
+ const gchar *interface, const gchar *method, GVariant *parameters,
45
+ GDBusMethodInvocation *invocation, gpointer user_data) {
46
+ (void)connection; (void)sender; (void)path; (void)interface; (void)user_data;
47
+
48
+ if (g_strcmp0(method, "Notify") == 0) {
49
+ const gchar *app = nullptr, *icon = nullptr, *summary = nullptr, *body = nullptr;
50
+ guint32 replaces = 0;
51
+ gint timeout = 0;
52
+ GVariantIter *actions = nullptr;
53
+ GVariant *hints = nullptr;
54
+ g_variant_get(parameters, "(&su&s&s&sas@a{sv}i)", &app, &replaces, &icon, &summary, &body,
55
+ &actions, &hints, &timeout);
56
+ printf("STUB Notify app=%s summary=%s body=%s\n", app, summary, body);
57
+ fflush(stdout);
58
+ if (actions != nullptr) g_variant_iter_free(actions);
59
+ if (hints != nullptr) g_variant_unref(hints);
60
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(u)", nextId++));
61
+ return;
62
+ }
63
+ if (g_strcmp0(method, "CloseNotification") == 0) {
64
+ guint32 id = 0;
65
+ g_variant_get(parameters, "(u)", &id);
66
+ printf("STUB CloseNotification id=%u\n", id);
67
+ fflush(stdout);
68
+ g_dbus_method_invocation_return_value(invocation, nullptr);
69
+ return;
70
+ }
71
+ if (g_strcmp0(method, "GetCapabilities") == 0) {
72
+ GVariantBuilder caps;
73
+ g_variant_builder_init(&caps, G_VARIANT_TYPE("as"));
74
+ g_variant_builder_add(&caps, "s", "body");
75
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(as)", &caps));
76
+ return;
77
+ }
78
+ g_dbus_method_invocation_return_dbus_error(invocation, "org.freedesktop.DBus.Error.UnknownMethod",
79
+ "no");
80
+ }
81
+
82
+ static const GDBusInterfaceVTable kVTable = {onCall, nullptr, nullptr, {nullptr}};
83
+
84
+ static void onBusAcquired(GDBusConnection *connection, const gchar *name, gpointer user_data) {
85
+ (void)name;
86
+ auto *info = static_cast<GDBusNodeInfo *>(user_data);
87
+ g_dbus_connection_register_object(connection, "/org/freedesktop/Notifications",
88
+ info->interfaces[0], &kVTable, nullptr, nullptr, nullptr);
89
+ }
90
+
91
+ static void onNameAcquired(GDBusConnection *c, const gchar *name, gpointer d) {
92
+ (void)c; (void)d;
93
+ printf("STUB ready %s\n", name);
94
+ fflush(stdout);
95
+ }
96
+
97
+ int main(void) {
98
+ GDBusNodeInfo *info = g_dbus_node_info_new_for_xml(kXml, nullptr);
99
+ g_bus_own_name(G_BUS_TYPE_SESSION, "org.freedesktop.Notifications", G_BUS_NAME_OWNER_FLAGS_NONE,
100
+ onBusAcquired, onNameAcquired, nullptr, info, nullptr);
101
+ g_main_loop_run(g_main_loop_new(nullptr, FALSE));
102
+ return 0;
103
+ }
@@ -0,0 +1,251 @@
1
+ // Tests for the accessibility mapping.
2
+ //
3
+ // GTK ships assertion helpers for exactly this (gtktestatcontext.h), so what a
4
+ // screen reader would be told can be read back rather than inferred. That is
5
+ // unusual and worth using: accessibility is otherwise the easiest thing in a UI
6
+ // to believe you have done.
7
+
8
+ #include "TestHarness.h"
9
+
10
+ #include "GtkMountingManager.h"
11
+
12
+ #include <react/renderer/components/iostextinput/TextInputProps.h>
13
+ #include <react/renderer/components/view/ViewProps.h>
14
+ #include <react/renderer/core/RawProps.h>
15
+ #include <react/renderer/core/RawPropsParser.h>
16
+
17
+ #include <sstream>
18
+
19
+ using facebook::react::AccessibilityState;
20
+ using facebook::react::ImportantForAccessibility;
21
+ using facebook::react::LayoutMetrics;
22
+ using facebook::react::MountingTransaction;
23
+ using facebook::react::ShadowView;
24
+ using facebook::react::ShadowViewMutation;
25
+ using facebook::react::ShadowViewMutationList;
26
+ using facebook::react::SurfaceId;
27
+ using facebook::react::Tag;
28
+ using facebook::react::TransactionTelemetry;
29
+ using facebook::react::ViewProps;
30
+
31
+ namespace {
32
+
33
+ constexpr SurfaceId kSurfaceId = 1;
34
+
35
+ // Builds a view with whatever accessibility props the caller wants to set.
36
+ ShadowView makeAccessibleView(Tag tag, const std::function<void(ViewProps &)> &configure) {
37
+ auto props = std::make_shared<ViewProps>();
38
+ configure(*props);
39
+
40
+ LayoutMetrics metrics;
41
+ metrics.frame = {.origin = {.x = 0, .y = 0}, .size = {.width = 50, .height = 50}};
42
+
43
+ ShadowView view;
44
+ view.componentName = "View";
45
+ view.surfaceId = kSurfaceId;
46
+ view.tag = tag;
47
+ view.props = props;
48
+ view.layoutMetrics = metrics;
49
+ return view;
50
+ }
51
+
52
+ // The same, for a <TextInput>. Its props have to be real TextInputProps or the
53
+ // mounting manager builds no peer, and those are only reachable through React
54
+ // Native's own parser -- the fields that matter are const.
55
+ ShadowView makeTextInputView(Tag tag, const std::function<void(ViewProps &)> & /*configure*/,
56
+ const std::string &label) {
57
+ static const facebook::react::RawPropsParser parser = []() {
58
+ facebook::react::RawPropsParser prepared;
59
+ prepared.prepare<facebook::react::TextInputProps>();
60
+ return prepared;
61
+ }();
62
+
63
+ static const auto contextContainer =
64
+ std::make_shared<const facebook::react::ContextContainer>();
65
+ const facebook::react::PropsParserContext context{kSurfaceId, *contextContainer};
66
+
67
+ folly::dynamic raw = folly::dynamic::object("text", "")("accessibilityLabel", label);
68
+ facebook::react::RawProps rawProps{std::move(raw)};
69
+ rawProps.parse(parser);
70
+
71
+ LayoutMetrics metrics;
72
+ metrics.frame = {.origin = {.x = 0, .y = 0}, .size = {.width = 200, .height = 40}};
73
+
74
+ ShadowView view;
75
+ view.componentName = "TextInput";
76
+ view.surfaceId = kSurfaceId;
77
+ view.tag = tag;
78
+ view.props = std::make_shared<const facebook::react::TextInputProps>(
79
+ context, facebook::react::TextInputProps{}, rawProps);
80
+ view.layoutMetrics = metrics;
81
+ return view;
82
+ }
83
+
84
+ // Mounts one view and hands back the widget.
85
+ RnView *mountOne(basalt::GtkMountingManager &manager, const ShadowView &view) {
86
+ ShadowViewMutationList mutations;
87
+ mutations.push_back(ShadowViewMutation::CreateMutation(view));
88
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, view, 0));
89
+ manager.applyTransaction(
90
+ kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
91
+
92
+ RnView *root = manager.getSurfaceRoot(kSurfaceId);
93
+ return RN_VIEW(gtk_widget_get_first_child(GTK_WIDGET(root)));
94
+ }
95
+
96
+ } // namespace
97
+
98
+ TEST(accessibility_role_maps_to_a_gtk_role) {
99
+ basalt::GtkMountingManager manager;
100
+ manager.createSurfaceRoot(kSurfaceId);
101
+
102
+ RnView *button = mountOne(manager, makeAccessibleView(10, [](ViewProps &props) {
103
+ props.accessibilityRole = "button";
104
+ }));
105
+ EXPECT_EQ(static_cast<int>(gtk_accessible_get_accessible_role(GTK_ACCESSIBLE(button))),
106
+ static_cast<int>(GTK_ACCESSIBLE_ROLE_BUTTON));
107
+
108
+ manager.destroySurfaceRoot(kSurfaceId);
109
+ }
110
+
111
+ TEST(unknown_accessibility_role_falls_back_to_generic) {
112
+ basalt::GtkMountingManager manager;
113
+ manager.createSurfaceRoot(kSurfaceId);
114
+
115
+ // A wrong role is worse than no role: it makes a widget announce itself as
116
+ // something it is not.
117
+ RnView *view = mountOne(manager, makeAccessibleView(11, [](ViewProps &props) {
118
+ props.accessibilityRole = "somethingReactNativeInvented";
119
+ }));
120
+ EXPECT_EQ(static_cast<int>(gtk_accessible_get_accessible_role(GTK_ACCESSIBLE(view))),
121
+ static_cast<int>(GTK_ACCESSIBLE_ROLE_GENERIC));
122
+
123
+ manager.destroySurfaceRoot(kSurfaceId);
124
+ }
125
+
126
+ TEST(accessibility_label_and_hint_reach_the_accessible) {
127
+ basalt::GtkMountingManager manager;
128
+ manager.createSurfaceRoot(kSurfaceId);
129
+
130
+ RnView *view = mountOne(manager, makeAccessibleView(12, [](ViewProps &props) {
131
+ props.accessibilityLabel = "Play";
132
+ props.accessibilityHint = "Starts the track";
133
+ }));
134
+
135
+ char *mismatch = gtk_test_accessible_check_property(
136
+ GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_PROPERTY_LABEL, "Play");
137
+ EXPECT(mismatch == nullptr);
138
+ g_free(mismatch);
139
+
140
+ mismatch = gtk_test_accessible_check_property(
141
+ GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, "Starts the track");
142
+ EXPECT(mismatch == nullptr);
143
+ g_free(mismatch);
144
+
145
+ manager.destroySurfaceRoot(kSurfaceId);
146
+ }
147
+
148
+ // A field's label has to be on the field, not on the view wrapping it. The
149
+ // peer is a real GtkText and is what AT-SPI presents as a text box, so a label
150
+ // left on the wrapper is a labelled field that announces no name.
151
+ TEST(a_text_input_label_lands_on_the_peer_not_the_wrapper) {
152
+ basalt::GtkMountingManager manager;
153
+ manager.createSurfaceRoot(kSurfaceId);
154
+
155
+ RnView *view = mountOne(manager, makeTextInputView(13, nullptr, "Your name"));
156
+ GtkWidget *peer = rn_view_get_editable(view);
157
+ EXPECT(peer != nullptr);
158
+
159
+ char *mismatch = gtk_test_accessible_check_property(
160
+ GTK_ACCESSIBLE(peer), GTK_ACCESSIBLE_PROPERTY_LABEL, "Your name");
161
+ EXPECT(mismatch == nullptr);
162
+ g_free(mismatch);
163
+
164
+ // And not announced twice: the wrapper does not also carry the name. Checked
165
+ // by asking for a mismatch rather than for an empty string, because an unset
166
+ // GTK property is not the empty one and does not compare equal to it.
167
+ mismatch = gtk_test_accessible_check_property(
168
+ GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_PROPERTY_LABEL, "Your name");
169
+ EXPECT(mismatch != nullptr);
170
+ g_free(mismatch);
171
+
172
+ // The wrapper is scaffolding rather than a control, so it is presentational
173
+ // and a screen reader walks past it to the peer. Decided at construction
174
+ // because a GtkAccessible role cannot be changed later.
175
+ EXPECT_EQ(static_cast<int>(gtk_accessible_get_accessible_role(GTK_ACCESSIBLE(view))),
176
+ static_cast<int>(GTK_ACCESSIBLE_ROLE_PRESENTATION));
177
+
178
+ // And the tree dump says nothing about it. React Native has no role here to
179
+ // report, AppKit prints none either, and a name invented for GTK's benefit
180
+ // would make the two hosts disagree on this view alone.
181
+ std::ostringstream dump;
182
+ char *text = rn_view_describe_tree(view);
183
+ dump << text;
184
+ g_free(text);
185
+ EXPECT(dump.str().find("role=") == std::string::npos);
186
+
187
+ manager.destroySurfaceRoot(kSurfaceId);
188
+ }
189
+
190
+ TEST(accessibility_state_reaches_the_accessible) {
191
+ basalt::GtkMountingManager manager;
192
+ manager.createSurfaceRoot(kSurfaceId);
193
+
194
+ RnView *view = mountOne(manager, makeAccessibleView(13, [](ViewProps &props) {
195
+ AccessibilityState state;
196
+ state.disabled = true;
197
+ state.selected = true;
198
+ state.checked = AccessibilityState::Checked;
199
+ props.accessibilityState = state;
200
+ }));
201
+
202
+ EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_DISABLED));
203
+ EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_SELECTED));
204
+ EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_CHECKED));
205
+
206
+ manager.destroySurfaceRoot(kSurfaceId);
207
+ }
208
+
209
+ TEST(no_accessibility_state_leaves_checked_unset) {
210
+ basalt::GtkMountingManager manager;
211
+ manager.createSurfaceRoot(kSurfaceId);
212
+
213
+ // A view that never says anything about "checked" is not an unchecked
214
+ // checkbox, and a screen reader should not announce it as one.
215
+ RnView *view = mountOne(manager, makeAccessibleView(14, [](ViewProps &props) { (void)props; }));
216
+
217
+ EXPECT(!gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_CHECKED));
218
+
219
+ manager.destroySurfaceRoot(kSurfaceId);
220
+ }
221
+
222
+ TEST(elements_hidden_marks_the_view_hidden) {
223
+ basalt::GtkMountingManager manager;
224
+ manager.createSurfaceRoot(kSurfaceId);
225
+
226
+ RnView *view = mountOne(manager, makeAccessibleView(15, [](ViewProps &props) {
227
+ props.accessibilityElementsHidden = true;
228
+ }));
229
+ EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_HIDDEN));
230
+
231
+ RnView *shown = mountOne(manager, makeAccessibleView(16, [](ViewProps &props) { (void)props; }));
232
+ char *mismatch = gtk_test_accessible_check_state(
233
+ GTK_ACCESSIBLE(shown), GTK_ACCESSIBLE_STATE_HIDDEN, FALSE);
234
+ EXPECT(mismatch == nullptr);
235
+ g_free(mismatch);
236
+
237
+ manager.destroySurfaceRoot(kSurfaceId);
238
+ }
239
+
240
+ TEST(important_for_accessibility_no_hide_descendants_hides_too) {
241
+ basalt::GtkMountingManager manager;
242
+ manager.createSurfaceRoot(kSurfaceId);
243
+
244
+ RnView *view = mountOne(manager, makeAccessibleView(17, [](ViewProps &props) {
245
+ props.importantForAccessibility =
246
+ ImportantForAccessibility::NoHideDescendants;
247
+ }));
248
+ EXPECT(gtk_test_accessible_has_state(GTK_ACCESSIBLE(view), GTK_ACCESSIBLE_STATE_HIDDEN));
249
+
250
+ manager.destroySurfaceRoot(kSurfaceId);
251
+ }