summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 22:12:02 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 22:12:02 +0300
commit94c5e361fbb229b0cd6853e2102a7d95f1f358b3 (patch)
tree3c0f11af9dcb84dac8067a31a6979acd6dd56fbf /tests
parent08dc9047098d6d96791024ac7d8f5837633673ef (diff)
better public interface and doxygen
Diffstat (limited to 'tests')
-rw-r--r--tests/cocoa/test_cursor.m12
-rw-r--r--tests/cocoa/test_keyboard.m134
-rw-r--r--tests/common/test_backends.c90
-rw-r--r--tests/common/test_contracts.c92
-rw-r--r--tests/common/test_keyboard.c32
-rw-r--r--tests/common/test_utf8.c27
-rw-r--r--tests/helpers/check_exports.py11
-rw-r--r--tests/helpers/package_cocoa_tests.py70
-rw-r--r--tests/helpers/run_backends.py8
-rw-r--r--tests/helpers/run_cocoa_bundle.py68
-rw-r--r--tests/meson.build47
-rw-r--r--tests/test_api.cpp13
-rw-r--r--tests/wayland/test_clipboard.c47
-rw-r--r--tests/wayland/test_keyboard.c128
-rw-r--r--tests/wayland/test_tablet.c37
-rw-r--r--tests/x11/test_input.c123
-rw-r--r--tests/x11/test_tablet.c13
17 files changed, 934 insertions, 18 deletions
diff --git a/tests/cocoa/test_cursor.m b/tests/cocoa/test_cursor.m
index 0f3ba06..7a328f2 100644
--- a/tests/cocoa/test_cursor.m
+++ b/tests/cocoa/test_cursor.m
@@ -22,8 +22,12 @@ int main(void)
assert(surface);
NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface);
NSView *view = window.contentView;
+ NSEvent *cursor = [NSEvent enterExitEventWithType:NSEventTypeCursorUpdate
+ location:NSMakePoint(100, 120) modifierFlags:0 timestamp:0
+ windowNumber:window.windowNumber context:nil eventNumber:0 trackingNumber:0 userData:NULL];
+ assert(cursor);
yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN);
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
NSCursor *hidden = [NSCursor currentCursor];
assert_transparent(hidden);
@@ -31,7 +35,7 @@ int main(void)
location:NSMakePoint(100, 120) modifierFlags:0 timestamp:0
windowNumber:window.windowNumber context:nil eventNumber:0 clickCount:0 pressure:0];
[view mouseMoved:motion];
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
assert([NSCursor currentCursor] == hidden);
struct yasl_input_event event;
bool moved = false;
@@ -41,10 +45,10 @@ int main(void)
assert(moved);
[[NSCursor arrowCursor] set]; /* Simulate leaving and re-entering. */
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
assert([NSCursor currentCursor] == hidden);
yasl_surface_set_cursor(surface, YASL_CURSOR_ARROW);
- [view cursorUpdate:nil];
+ [view cursorUpdate:cursor];
assert([NSCursor currentCursor] == [NSCursor arrowCursor]);
yasl_surface_destroy(surface);
yasl_display_close(display);
diff --git a/tests/cocoa/test_keyboard.m b/tests/cocoa/test_keyboard.m
new file mode 100644
index 0000000..4854065
--- /dev/null
+++ b/tests/cocoa/test_keyboard.m
@@ -0,0 +1,134 @@
+#import <AppKit/AppKit.h>
+#include <yasl/native.h>
+#include <assert.h>
+#include <math.h>
+#include <string.h>
+
+/* Reject character queries on modifier-only events, as AppKit does. */
+@interface InputSample : NSObject
+@property NSEventType type;
+@property unsigned short keyCode;
+@property NSEventModifierFlags modifierFlags;
+@property(copy) NSString *label;
+@property(copy) NSString *shiftedLabel;
+@property NSPoint locationInWindow;
+@property CGFloat scrollingDeltaX;
+@property CGFloat scrollingDeltaY;
+@property BOOL hasPreciseScrollingDeltas;
+@end
+
+@implementation InputSample
+- (NSString *)charactersByApplyingModifiers:(NSEventModifierFlags)modifiers
+{
+ assert(self.type == NSEventTypeKeyDown || self.type == NSEventTypeKeyUp);
+ return modifiers & NSEventModifierFlagShift ? self.shiftedLabel : self.label;
+}
+- (void)dealloc
+{
+ [_label release];
+ [_shiftedLabel release];
+ [super dealloc];
+}
+@end
+
+static struct yasl_input_event next(struct yasl_surface *surface, enum yasl_input_kind kind)
+{
+ struct yasl_input_event event;
+ assert(yasl_surface_input_poll(surface, &event));
+ assert(event.kind == kind && event.surface == surface);
+ return event;
+}
+
+static void empty(struct yasl_surface *surface)
+{
+ struct yasl_input_event event;
+ assert(!yasl_surface_input_poll(surface, &event));
+}
+
+int main(void)
+{
+ @autoreleasepool {
+ struct yasl_display *display = yasl_display_open(NULL, NULL);
+ assert(display);
+ struct yasl_surface_spec spec = {.title = "Keyboard test", .size = {320, 240}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec);
+ assert(surface);
+ NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface);
+ NSView<NSTextInputClient> *view = (id)window.contentView;
+ struct yasl_input_event event;
+ while (yasl_surface_input_poll(surface, &event)) {}
+
+ InputSample *sample = [InputSample new];
+ NSEvent *native = (NSEvent *)sample;
+ sample.type = NSEventTypeKeyUp;
+ sample.keyCode = 0; /* US A, even with a different active layout. */
+ sample.label = @"q";
+ sample.shiftedLabel = @"Q";
+ [view keyUp:native];
+ event = next(surface, YASL_INPUT_KEY);
+ assert(event.key.key == YASL_KEY_A && event.key.native_code == 0);
+ assert(event.key.action == YASL_KEY_RELEASE);
+ assert(event.key.codepoint == 'q' && event.key.shifted == 'Q');
+ empty(surface);
+ sample.label = @"😀"; /* One scalar, two UTF-16 units. */
+ sample.shiftedLabel = @"ab";
+ [view keyUp:native];
+ event = next(surface, YASL_INPUT_KEY);
+ assert(event.key.codepoint == 0x1f600 && event.key.shifted == 0);
+ sample.type = NSEventTypeFlagsChanged;
+ sample.keyCode = 56; /* Left Shift. */
+ sample.modifierFlags = NSEventModifierFlagShift | 0x0002;
+ [view flagsChanged:native];
+ event = next(surface, YASL_INPUT_KEY);
+ assert(event.key.key == YASL_KEY_LEFT_SHIFT && event.key.action == YASL_KEY_PRESS);
+ assert(event.key.mods == YASL_MOD_SHIFT && !event.key.codepoint && !event.key.shifted);
+ sample.modifierFlags = 0;
+ [view flagsChanged:native];
+ assert(next(surface, YASL_INPUT_KEY).key.action == YASL_KEY_RELEASE);
+
+ /* Native text services own preedit and commits. Marked text is not an
+ * insertion, and commits retain all UTF-8 bytes, including non-BMP. */
+ [view setMarkedText:@"é" selectedRange:NSMakeRange(1, 0)
+ replacementRange:NSMakeRange(NSNotFound, 0)];
+ assert(strcmp(yasl_surface_ime_preedit(surface), "é") == 0);
+ empty(surface);
+ [view insertText:@"😀é" replacementRange:NSMakeRange(NSNotFound, 0)];
+ event = next(surface, YASL_INPUT_TEXT);
+ assert(event.text.len == strlen("😀é") && strcmp(event.text.text, "😀é") == 0);
+ assert(!yasl_surface_ime_preedit(surface));
+ empty(surface);
+
+ /* Exercise the keyDown -> interpretKeyEvents -> insertText route with
+ * an actual AppKit key event rather than the strict event double. */
+ NSEvent *press = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSZeroPoint
+ modifierFlags:0 timestamp:0 windowNumber:window.windowNumber context:nil
+ characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:0];
+ [view keyDown:press];
+ assert(next(surface, YASL_INPUT_KEY).key.key == YASL_KEY_A);
+ event = next(surface, YASL_INPUT_TEXT);
+ assert(event.text.len == 1 && event.text.text[0] == 'a');
+ empty(surface);
+
+ sample.type = NSEventTypeScrollWheel;
+ sample.hasPreciseScrollingDeltas = YES;
+ sample.scrollingDeltaX = -2.5;
+ sample.scrollingDeltaY = 1.25;
+ [view scrollWheel:native];
+ event = next(surface, YASL_INPUT_MOUSE);
+ assert(event.mouse.kind == YASL_PTR_SCROLL);
+ assert(event.mouse.scroll.x == 0.25 && event.mouse.scroll.y == -0.125);
+ sample.hasPreciseScrollingDeltas = NO;
+ sample.scrollingDeltaX = 1;
+ sample.scrollingDeltaY = -1;
+ [view scrollWheel:native];
+ event = next(surface, YASL_INPUT_MOUSE);
+ assert(event.mouse.scroll.x == -1 && event.mouse.scroll.y == 1);
+ sample.scrollingDeltaX = NAN;
+ [view scrollWheel:native];
+ empty(surface);
+ [sample release];
+ yasl_surface_destroy(surface);
+ yasl_display_close(display);
+ }
+ return 0;
+}
diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c
index 4c99646..cf5b1f9 100644
--- a/tests/common/test_backends.c
+++ b/tests/common/test_backends.c
@@ -194,23 +194,105 @@ static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct
yasl_display_copy(d, YASL_CLIPBOARD, "clipboard text");
yasl_display_paste(d, YASL_CLIPBOARD, w->surface);
pump(loop, d, NULL);
- bool saw_key = false, saw_paste = false;
+ bool saw_key = false, saw_paste = false, saw_text = false;
struct yasl_input_event event;
while (yasl_surface_input_poll(w->surface, &event)) {
assert(event.surface == w->surface);
if (event.kind == YASL_INPUT_KEY) {
assert(event.key.codepoint == 'a');
assert(event.key.shifted == 'A');
- assert(event.key.base == 30);
- saw_key = event.key.key == XK_a;
+ assert(event.key.native_code == key.xkey.keycode);
+ saw_key = event.key.key == YASL_KEY_A;
}
if (event.kind == YASL_INPUT_PASTE) {
assert(event.paste.len == strlen("clipboard text"));
assert(memcmp(event.paste.text, "clipboard text", event.paste.len) == 0);
saw_paste = true;
}
+ if (event.kind == YASL_INPUT_TEXT) {
+ assert(event.text.len == 1 && event.text.text[0] == 'a');
+ saw_text = true;
+ }
+ }
+ assert(saw_key && saw_paste && saw_text);
+ /* Physical identity survives modifier and layout changes, including a
+ * release whose modifiers differ from its press. */
+ key.xkey.state = ShiftMask;
+ XSendEvent(native, window, False, KeyPressMask, &key);
+ key.xkey.type = KeyRelease;
+ key.xkey.state = 0;
+ XSendEvent(native, window, False, KeyReleaseMask, &key);
+ XFlush(native);
+ pump(loop, d, NULL);
+ int key_count = 0, text_count = 0;
+ while (yasl_surface_input_poll(w->surface, &event)) {
+ if (event.kind == YASL_INPUT_KEY) {
+ assert(event.key.key == YASL_KEY_A);
+ assert(event.key.action == (key_count ? YASL_KEY_RELEASE : YASL_KEY_PRESS));
+ key_count++;
+ } else if (event.kind == YASL_INPUT_TEXT) {
+ assert(event.text.len == 1 && event.text.text[0] == 'A');
+ text_count++;
+ }
+ }
+ assert(key_count == 2 && text_count == 1);
+ key.xkey.type = KeyPress;
+ key.xkey.state = ControlMask;
+ XSendEvent(native, window, False, KeyPressMask, &key);
+ XFlush(native);
+ pump(loop, d, NULL);
+ assert(yasl_surface_input_poll(w->surface, &event));
+ assert(event.kind == YASL_INPUT_KEY && event.key.key == YASL_KEY_A);
+ assert(event.key.mods & YASL_MOD_CTRL);
+ assert(!yasl_surface_input_poll(w->surface, &event));
+
+ int symbols_per_key = 0;
+ KeySym *saved_symbols = XGetKeyboardMapping(native, key.xkey.keycode, 1, &symbols_per_key);
+ assert(saved_symbols && symbols_per_key >= 2);
+ KeySym *changed_symbols = calloc((size_t)symbols_per_key, sizeof(KeySym));
+ assert(changed_symbols);
+ changed_symbols[0] = XK_q;
+ changed_symbols[1] = XK_Q;
+ XChangeKeyboardMapping(native, key.xkey.keycode, symbols_per_key, changed_symbols, 1);
+ XSync(native, False);
+ pump(loop, d, NULL); /* Consume MappingNotify before translating the key. */
+ key.xkey.state = 0;
+ XSendEvent(native, window, False, KeyPressMask, &key);
+ XFlush(native);
+ pump(loop, d, NULL);
+ saw_key = saw_text = false;
+ while (yasl_surface_input_poll(w->surface, &event)) {
+ if (event.kind == YASL_INPUT_KEY) {
+ assert(event.key.key == YASL_KEY_A && event.key.codepoint == 'q');
+ saw_key = true;
+ } else if (event.kind == YASL_INPUT_TEXT) {
+ assert(event.text.len == 1 && event.text.text[0] == 'q');
+ saw_text = true;
+ }
+ }
+ assert(saw_key && saw_text);
+ XChangeKeyboardMapping(native, key.xkey.keycode, symbols_per_key, saved_symbols, 1);
+ free(changed_symbols);
+ XFree(saved_symbols);
+ XSync(native, False);
+ pump(loop, d, NULL);
+
+ for (unsigned button = 4; button <= 7; button++) {
+ XEvent wheel = {.xbutton = {.type = ButtonPress, .display = native,
+ .window = window, .root = DefaultRootWindow(native),
+ .same_screen = True, .button = button}};
+ /* Direct delivery exercises the core fallback even with XI2 enabled. */
+ XSendEvent(native, window, False, NoEventMask, &wheel);
+ wheel.xbutton.type = ButtonRelease;
+ XSendEvent(native, window, False, NoEventMask, &wheel);
+ XFlush(native);
+ pump(loop, d, NULL);
+ assert(yasl_surface_input_poll(w->surface, &event));
+ assert(event.kind == YASL_INPUT_MOUSE && event.mouse.kind == YASL_PTR_SCROLL);
+ assert(event.mouse.scroll.x == (button == 6 ? -1 : button == 7 ? 1 : 0));
+ assert(event.mouse.scroll.y == (button == 4 ? -1 : button == 5 ? 1 : 0));
+ assert(!yasl_surface_input_poll(w->surface, &event));
}
- assert(saw_key && saw_paste);
assert(yasl_surface_timing(w->surface).source == YASL_TIMING_SUBMISSION);
assert(!(yasl_display_caps(d) & YASL_CAP_FRAME_PACING));
diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c
index 4e22de1..8580dd6 100644
--- a/tests/common/test_contracts.c
+++ b/tests/common/test_contracts.c
@@ -232,6 +232,97 @@ static void test_scale(void)
assert(yasl_scale_to_logical(1, 0) == 0);
}
+static int popup_calls;
+static struct yasl_surface *popup_create(struct yasl_surface *parent,
+ const struct yasl_popup_spec *spec)
+{
+ (void)parent; (void)spec;
+ popup_calls++;
+ return NULL; /* Backend fails without setting errno. */
+}
+
+static bool popup_reposition(struct yasl_surface *popup,
+ const struct yasl_popup_spec *spec)
+{
+ (void)popup; (void)spec;
+ popup_calls++;
+ errno = EBUSY;
+ return false;
+}
+
+static uint32_t popup_caps(const struct yasl_display *d)
+{ (void)d; return YASL_CAP_POPUPS; }
+
+static int dispatch_failure(struct yasl_display *d, int timeout)
+{ (void)d; (void)timeout; return -1; }
+
+static struct yasl_surface *create_failure(struct yasl_display *d,
+ const struct yasl_surface_spec *spec, void *device)
+{ (void)d; (void)spec; (void)device; return NULL; }
+
+static int drag_starts;
+static bool drag_without_listener(struct yasl_display *d, struct yasl_surface *s,
+ const struct yasl_drag_image *image, const struct yasl_drag_listener *listener,
+ void *ud)
+{
+ (void)d; (void)s; (void)image; (void)ud;
+ /* Backends may require a table even when no completion callback is needed. */
+ assert(listener && !listener->end);
+ drag_starts++;
+ return true;
+}
+
+static void test_errors(void)
+{
+ const struct yasl_backend_ops ops = {.display_caps = popup_caps,
+ .surface_create_toplevel = create_failure,
+ .surface_create_popup = popup_create,
+ .surface_popup_reposition = popup_reposition,
+ .display_drag_start = drag_without_listener,
+ .display_dispatch_timeout = dispatch_failure};
+ struct yasl_display d = {.ops = &ops};
+ struct yasl_surface s = {.display = &d};
+ struct yasl_popup_spec spec = {.size = {120, 80}, .anchor = {0, 0, 1, 1}};
+ assert(!yasl_surface_create_popup(NULL, &spec) && errno == EINVAL);
+ assert(!yasl_surface_create_popup(&s, NULL) && errno == EINVAL);
+ spec.anchor.width = -1; /* Must not reach Wayland's protocol marshaler. */
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL);
+ assert(!yasl_surface_popup_reposition(&s, &spec) && errno == EINVAL);
+ spec.anchor.width = 1;
+ spec.content_offset.x = INT_MAX;
+ spec.content_size.x = INT_MAX;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL);
+ spec.content_offset.x = spec.content_size.x = 0;
+ spec.size.x = 32768;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL);
+ assert(!popup_calls);
+ spec.size.x = 120;
+ errno = ERANGE;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == ENODEV);
+ assert(!yasl_surface_popup_reposition(&s, &spec) && errno == EBUSY);
+ assert(popup_calls == 2);
+ const struct yasl_backend_ops unsupported = {0};
+ d.ops = &unsupported;
+ assert(!yasl_surface_create_popup(&s, &spec) && errno == ENOTSUP);
+ assert(!yasl_surface_popup_reposition(&s, &spec) && errno == ENOTSUP);
+ assert(yasl_display_dispatch(&d, 0) == -1 && errno == ENOTSUP);
+ assert(yasl_display_dispatch(NULL, 0) == -1 && errno == EINVAL);
+ d.ops = &ops;
+ errno = ERANGE;
+ assert(yasl_display_dispatch(&d, 0) == -1 && errno == EIO);
+ struct yasl_surface_spec toplevel = {.size = {120, 80}};
+ errno = ERANGE;
+ assert(!yasl_surface_create_toplevel(&d, &toplevel) && errno == ENODEV);
+ assert(yasl_display_drag_start(&d, &s, NULL, NULL, NULL));
+ struct yasl_drag_image image = {.size = {1, 1}};
+ assert(!yasl_display_drag_start(&d, &s, &image, NULL, NULL));
+ uint32_t pixel = 0;
+ image.pixels = &pixel;
+ image.size.x = INT_MAX;
+ assert(!yasl_display_drag_start(&d, &s, &image, NULL, NULL));
+ assert(drag_starts == 1);
+}
+
int main(void)
{
test_timing_clock();
@@ -239,5 +330,6 @@ int main(void)
test_validation();
test_native();
test_scale();
+ test_errors();
return 0;
}
diff --git a/tests/common/test_keyboard.c b/tests/common/test_keyboard.c
new file mode 100644
index 0000000..5a7f18e
--- /dev/null
+++ b/tests/common/test_keyboard.c
@@ -0,0 +1,32 @@
+#include "common/keyboard.h"
+#include "cocoa/keycodes.h"
+#include <assert.h>
+
+int main(void)
+{
+ /* Same physical positions across native encodings, including keypad and
+ * left/right modifiers. macOS A is code zero, which is not UNKNOWN. */
+ assert(yasl_key_from_evdev(30) == YASL_KEY_A);
+ assert(yasl_key_from_cocoa(kVK_ANSI_A) == YASL_KEY_A);
+ assert(yasl_key_from_xkb_name("AC01") == YASL_KEY_A);
+ assert(yasl_key_from_evdev(1) == YASL_KEY_ESCAPE);
+ assert(yasl_key_from_cocoa(kVK_Escape) == YASL_KEY_ESCAPE);
+ assert(yasl_key_from_xkb_name("ESC\0") == YASL_KEY_ESCAPE);
+ assert(yasl_key_from_evdev(96) == YASL_KEY_KP_ENTER);
+ assert(yasl_key_from_cocoa(kVK_ANSI_KeypadEnter) == YASL_KEY_KP_ENTER);
+ assert(yasl_key_from_cocoa(kVK_Return) == YASL_KEY_ENTER);
+ assert(yasl_key_from_evdev(100) == YASL_KEY_RIGHT_ALT);
+ assert(yasl_key_from_cocoa(kVK_RightOption) == YASL_KEY_RIGHT_ALT);
+ assert(yasl_key_from_evdev(42) == YASL_KEY_LEFT_SHIFT);
+ assert(yasl_key_from_cocoa(kVK_Shift) == YASL_KEY_LEFT_SHIFT);
+ assert(yasl_key_from_evdev(86) == YASL_KEY_ISO_BACKSLASH);
+ assert(yasl_key_from_cocoa(kVK_ISO_Section) == YASL_KEY_ISO_BACKSLASH);
+ assert(yasl_key_from_evdev(0) == YASL_KEY_UNKNOWN);
+ assert(yasl_key_from_evdev(UINT32_MAX) == YASL_KEY_UNKNOWN);
+ assert(yasl_key_from_cocoa(UINT32_MAX) == YASL_KEY_UNKNOWN);
+ assert(yasl_key_from_xkb_name("ZZZZ") == YASL_KEY_UNKNOWN);
+ assert(yasl_key_label(0x1f600) == 0x1f600);
+ assert(yasl_key_label(0xd800) == 0 && yasl_key_label(0x110000) == 0);
+ assert(yasl_key_label(0xf700) == 0 && yasl_key_label('\t') == 0);
+ return 0;
+}
diff --git a/tests/common/test_utf8.c b/tests/common/test_utf8.c
new file mode 100644
index 0000000..72f8904
--- /dev/null
+++ b/tests/common/test_utf8.c
@@ -0,0 +1,27 @@
+#include "common/utf8.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(void)
+{
+ char latin1[256];
+ for (int i = 1; i <= 255; i++) latin1[i - 1] = (char)i;
+ latin1[255] = '\0';
+ size_t len = 0, back_len = 0;
+ char *utf8 = yasl_utf8_from_latin1(latin1, 255, &len);
+ assert(utf8 && yasl_utf8_validate(utf8, len));
+ char *back = yasl_utf8_to_latin1(utf8, &back_len);
+ assert(back && back_len == 255 && memcmp(back, latin1, 256) == 0);
+ free(back);
+ free(utf8);
+ assert(!yasl_utf8_to_latin1("😀", &len));
+ assert(!yasl_utf8_to_latin1("\xc3", &len));
+ assert(!yasl_utf8_validate("\xc3", 1));
+ assert(!yasl_utf8_validate("\xe2\x82", 2));
+ assert(!yasl_utf8_validate("\xf0\x9f\x98", 3));
+ assert(!yasl_utf8_validate("\xed\xa0\x80", 3)); /* Surrogate. */
+ assert(yasl_utf8_validate("😀", sizeof("😀") - 1));
+ assert(yasl_utf8_validate(NULL, 0));
+ return 0;
+}
diff --git a/tests/helpers/check_exports.py b/tests/helpers/check_exports.py
index 0d10014..acb727d 100644
--- a/tests/helpers/check_exports.py
+++ b/tests/helpers/check_exports.py
@@ -9,6 +9,7 @@ import subprocess
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--allow-export', action='append', default=[])
+ parser.add_argument('--platform', choices=['linux', 'darwin'], default='linux')
parser.add_argument('nm')
parser.add_argument('library')
parser.add_argument('headers', nargs='+')
@@ -23,10 +24,14 @@ def main():
if not expected:
raise SystemExit("No public function declarations found")
- output = subprocess.check_output(
- [args.nm, "-D", "--defined-only", "--format=posix", args.library], text=True
- )
+ nm_options = ["-g", "-U", "-P"] if args.platform == 'darwin' else [
+ "-D", "--defined-only", "--format=posix"
+ ]
+ output = subprocess.check_output([args.nm, *nm_options, args.library], text=True)
exported = {line.split()[0] for line in output.splitlines() if line.strip()}
+ if args.platform == 'darwin':
+ # Mach-O decorates C symbols with one leading underscore.
+ exported = {name[1:] if name.startswith('_') else name for name in exported}
missing = expected - exported
leaked = exported - expected - set(args.allow_export)
if missing or leaked:
diff --git a/tests/helpers/package_cocoa_tests.py b/tests/helpers/package_cocoa_tests.py
new file mode 100644
index 0000000..8f801f8
--- /dev/null
+++ b/tests/helpers/package_cocoa_tests.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+"""Package compiled Cocoa tests for execution on a matching macOS desktop."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import shutil
+import tarfile
+import tempfile
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('build', type=Path)
+ parser.add_argument('output', type=Path)
+ args = parser.parse_args()
+ build = args.build.resolve()
+ info = build / 'meson-info'
+ host = json.loads((info / 'intro-machines.json').read_text())['host']
+ if host['system'] != 'darwin':
+ parser.error('Use a configured and compiled Cocoa build.')
+ tests = json.loads((info / 'intro-tests.json').read_text())
+ required = {'native', 'native-ownership', 'keyboard-cocoa', 'tablet-cocoa',
+ 'cursor-cocoa', 'hello-cocoa-window', 'tablet-cocoa-window'}
+ missing = required - {test['name'] for test in tests}
+ if missing:
+ parser.error('Enable integration_tests; missing: ' + ', '.join(sorted(missing)))
+ options = json.loads((info / 'intro-buildoptions.json').read_text())
+ sanitizer = next(option['value'] for option in options if option['name'] == 'b_sanitize')
+ if sanitizer not in ('none', [], ['none']):
+ parser.error('Use an unsanitized build; sanitizer runtimes are not bundled.')
+
+ with tempfile.TemporaryDirectory(prefix='yasl-cocoa-bundle-') as directory:
+ root = Path(directory) / 'yasl-cocoa-tests'
+ root.mkdir()
+ files = {}
+
+ def copy(source, relative):
+ destination = root / relative
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(source, destination)
+ files[str(relative)] = hashlib.sha256(destination.read_bytes()).hexdigest()
+
+ copy(build / 'libyasl.dylib', Path('libyasl.dylib'))
+ source_root = Path(__file__).resolve().parents[2]
+ copy(source_root / 'LICENSE', Path('LICENSE'))
+ copy(Path(__file__).with_name('run_cocoa_bundle.py'), Path('run.py'))
+ commands = []
+ for test in tests:
+ if test['name'] == 'public-exports':
+ continue # Check the dylib with nm on the build machine first.
+ executable = Path(test['cmd'][0]).resolve()
+ try:
+ relative = executable.relative_to(build)
+ except ValueError:
+ parser.error('Unexpected external test command: ' + str(executable))
+ copy(executable, relative)
+ commands.append({'name': test['name'], 'cmd': [str(relative), *test['cmd'][1:]],
+ 'timeout': test['timeout']})
+ manifest = {'architecture': host['cpu_family'], 'minimum_macos': 14,
+ 'tests': commands, 'sha256': files}
+ (root / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
+ with tarfile.open(args.output, 'w:gz') as archive:
+ archive.add(root, arcname=root.name)
+ print(f'Packaged {len(commands)} runtime tests for {host["cpu_family"]}: {args.output}')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py
index 57cc9ab..70c82fb 100644
--- a/tests/helpers/run_backends.py
+++ b/tests/helpers/run_backends.py
@@ -21,7 +21,7 @@ def wait_for(process, ready):
def main():
- weston, xwayland, test, timers, tablet = sys.argv[1:]
+ weston, xwayland, test, timers, tablet, hello, input_x11 = sys.argv[1:]
servers = []
with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory:
root = Path(directory)
@@ -80,10 +80,16 @@ def main():
x_env["XDG_SESSION_TYPE"] = "x11"
print("Test: X11 surface timers", flush=True)
subprocess.run([timers], env=x_env, check=True, timeout=8)
+ print("Test: X11 UTF-8 keyboard fallback and clipboard", flush=True)
+ subprocess.run([input_x11], env=x_env, check=True, timeout=8)
for backend in ("wayland", "x11"):
print("Test: tablet example on", backend, flush=True)
subprocess.run([tablet, "--backend", backend, "--smoke-test"],
env=env, check=True, timeout=8)
+ print("Test: hello example on", backend, flush=True)
+ subprocess.run([hello, "--smoke-test"],
+ env=env if backend == "wayland" else x_env,
+ check=True, timeout=8)
except BaseException:
for log in (wl_log, x_log):
log.flush()
diff --git a/tests/helpers/run_cocoa_bundle.py b/tests/helpers/run_cocoa_bundle.py
new file mode 100644
index 0000000..338f38e
--- /dev/null
+++ b/tests/helpers/run_cocoa_bundle.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""Run a packaged Cocoa suite, preserving output and a machine-readable report."""
+
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import platform
+import re
+import subprocess
+import sys
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--list', action='store_true', help='List tests without executing them.')
+ args = parser.parse_args()
+ root = Path(__file__).resolve().parent
+ manifest = json.loads((root / 'manifest.json').read_text())
+ if args.list:
+ print('Architecture:', manifest['architecture'])
+ for test in manifest['tests']:
+ print(test['name'])
+ return
+ if sys.platform != 'darwin':
+ parser.error('Run this suite in a macOS desktop session.')
+ if int(platform.mac_ver()[0].split('.')[0]) < manifest['minimum_macos']:
+ parser.error('This suite requires macOS 14 or newer.')
+ architecture = {'arm64': 'aarch64'}.get(platform.machine(), platform.machine())
+ if architecture != manifest['architecture']:
+ parser.error('Use the bundle matching this Python process architecture.')
+ for relative, expected in manifest['sha256'].items():
+ if hashlib.sha256((root / relative).read_bytes()).hexdigest() != expected:
+ parser.error('Bundle file changed: ' + relative)
+
+ logs = root / 'logs'
+ logs.mkdir(exist_ok=True)
+ env = os.environ.copy()
+ env['DYLD_LIBRARY_PATH'] = str(root)
+ results = []
+ for test in manifest['tests']:
+ name = test['name']
+ log = logs / (re.sub(r'[^A-Za-z0-9_-]', '_', name) + '.log')
+ command = [str(root / test['cmd'][0]), *test['cmd'][1:]]
+ with log.open('w') as output:
+ try:
+ process = subprocess.run(command, cwd=root, env=env, stdout=output,
+ stderr=subprocess.STDOUT, timeout=test['timeout'] or None)
+ result = {'name': name, 'returncode': process.returncode,
+ 'status': 'pass' if process.returncode == 0 else 'fail'}
+ except subprocess.TimeoutExpired:
+ output.write('\nTest timed out.\n')
+ result = {'name': name, 'status': 'timeout'}
+ except OSError as error:
+ output.write(str(error) + '\n')
+ result = {'name': name, 'status': 'error'}
+ results.append(result)
+ print(f'{name}: {result["status"]}', flush=True)
+ report = root / 'results.json'
+ report.write_text(json.dumps(results, indent=2) + '\n')
+ failed = sum(result['status'] != 'pass' for result in results)
+ print(f'{len(results) - failed} passed, {failed} failed. Logs: {logs}')
+ sys.exit(1 if failed else 0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/meson.build b/tests/meson.build
index 910f17c..c6570d9 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -30,16 +30,20 @@ if add_languages('cpp', native: false, required: false)
'yasl-cpp-api-test',
'test_api.cpp',
dependencies: shared_dep,
- cpp_args: cpp.get_supported_arguments('-pedantic-errors'),
+ cpp_args: cpp.get_supported_arguments('-pedantic-errors') + [
+ '-DYASL_HAS_WAYLAND=' + (has_wayland ? '1' : '0'),
+ '-DYASL_HAS_X11=' + (has_x11 ? '1' : '0'),
+ '-DYASL_HAS_COCOA=' + (has_cocoa ? '1' : '0'),
+ ],
override_options: ['cpp_std=c++11'],
)
test('cpp-api', cpp_api_test)
endif
-python = find_program('python3')
-if system == 'linux'
- nm = find_program('nm')
- export_args = []
+python = find_program('python3', native: true)
+if system == 'linux' or system == 'darwin'
+ nm = find_program('nm', native: false)
+ export_args = ['--platform=' + system]
if get_option('b_sanitize').contains('address')
# Clang's ASan instrumentation exports these linker-generated markers.
export_args += ['--allow-export=__start_asan_globals',
@@ -57,6 +61,9 @@ if system == 'linux'
if has_x11
exported_headers += files('../include/yasl/native/x11.h')
endif
+ if has_cocoa
+ exported_headers += files('../include/yasl/native/cocoa.h')
+ endif
test('public-exports', python,
args: [files('helpers/check_exports.py')] + export_args +
[nm, yasl_lib.get_shared_lib(), exported_headers],
@@ -71,6 +78,16 @@ contracts_test = executable(
)
test('contracts', contracts_test)
+keyboard_test = executable('yasl-keyboard-test', 'common/test_keyboard.c',
+ include_directories: inc, dependencies: yasl_dep,
+)
+test('keyboard', keyboard_test)
+
+utf8_test = executable('yasl-utf8-test', 'common/test_utf8.c',
+ include_directories: inc, dependencies: yasl_dep,
+)
+test('utf8', utf8_test)
+
timer_test = executable(
'yasl-timer-test',
'common/test_timers.c',
@@ -118,6 +135,14 @@ elif get_option('x11_tablet_tests')
endif
if has_wayland
+ clipboard_wayland_test = executable('yasl-clipboard-wayland-test', 'wayland/test_clipboard.c',
+ include_directories: inc, dependencies: yasl_dep,
+ )
+ test('clipboard-wayland', clipboard_wayland_test)
+ keyboard_wayland_test = executable('yasl-keyboard-wayland-test', 'wayland/test_keyboard.c',
+ include_directories: inc, dependencies: yasl_dep,
+ )
+ test('keyboard-wayland', keyboard_wayland_test)
ime_test = executable('yasl-ime-test', 'wayland/test_input.c',
include_directories: inc,
dependencies: yasl_dep,
@@ -143,6 +168,9 @@ benchmark('performance', benchmark_exe)
if get_option('integration_tests')
if has_cocoa
+ test('hello-cocoa-window', hello_example,
+ args: ['--smoke-test'], timeout: 20,
+ )
native_test = executable('yasl-native-test', 'cocoa/test_native.c',
dependencies: shared_dep,
)
@@ -151,6 +179,10 @@ if get_option('integration_tests')
dependencies: [shared_dep, deps],
)
test('native-ownership', ownership_test, timeout: 20)
+ keyboard_cocoa_test = executable('yasl-keyboard-cocoa-test', 'cocoa/test_keyboard.m',
+ dependencies: [shared_dep, deps],
+ )
+ test('keyboard-cocoa', keyboard_cocoa_test, timeout: 20)
tablet_cocoa_test = executable('yasl-tablet-cocoa-test', 'cocoa/test_tablet.m',
dependencies: [shared_dep, deps],
)
@@ -171,11 +203,14 @@ if get_option('integration_tests')
'common/test_backends.c',
dependencies: [shared_dep, deps],
)
+ input_x11_test = executable('yasl-input-x11-test', 'x11/test_input.c',
+ include_directories: inc, dependencies: yasl_dep,
+ )
weston = find_program('weston')
xwayland = find_program('Xwayland')
test('backends', python,
args: [files('helpers/run_backends.py'), weston, xwayland,
- backend_test, timer_test, tablet_example],
+ backend_test, timer_test, tablet_example, hello_example, input_x11_test],
timeout: 60,
)
endif
diff --git a/tests/test_api.cpp b/tests/test_api.cpp
index 9fb8525..129a41e 100644
--- a/tests/test_api.cpp
+++ b/tests/test_api.cpp
@@ -5,6 +5,9 @@
#if YASL_HAS_X11
#include "yasl/native/x11.h"
#endif
+#if YASL_HAS_COCOA
+#include "yasl/native/cocoa.h"
+#endif
#include "yasl/loop.h"
#include <yasl/timing.h>
#include <yasl/yasl.h>
@@ -28,6 +31,10 @@ int main()
yasl_loop_destroy(loop);
yasl_input_event event{};
+ event.key.key = YASL_KEY_A;
+ assert(event.key.key == YASL_KEY_A);
+ event.mouse.scroll = {0.25, -0.5};
+ assert(event.mouse.scroll.x == 0.25 && event.mouse.scroll.y == -0.5);
assert(!yasl_surface_input_poll(nullptr, &event));
assert(!yasl_display_drag_start(nullptr, nullptr, nullptr, nullptr, nullptr));
assert(!yasl_surface_drag_carry(nullptr, {0, 0}));
@@ -42,5 +49,11 @@ int main()
assert(!yasl_x11_surface_get_window(nullptr));
#endif
+#if YASL_HAS_COCOA
+ assert(!yasl_cocoa_display_get_application(nullptr));
+ assert(!yasl_cocoa_surface_get_window(nullptr));
+ assert(!yasl_cocoa_surface_get_view(nullptr));
+#endif
+
return 0;
}
diff --git a/tests/wayland/test_clipboard.c b/tests/wayland/test_clipboard.c
new file mode 100644
index 0000000..553e84d
--- /dev/null
+++ b/tests/wayland/test_clipboard.c
@@ -0,0 +1,47 @@
+#include "../../src/wayland/clipboard.c"
+#include <assert.h>
+
+struct received { int count; size_t len; char text[32]; };
+
+static void receive(const char *text, size_t len, bool uri_list,
+ struct wl_surface *surface, void *ud)
+{
+ (void)surface;
+ struct received *r = ud;
+ assert(!uri_list && len < sizeof(r->text));
+ memcpy(r->text, text, len);
+ r->text[len] = '\0';
+ r->len = len;
+ r->count++;
+}
+
+int main(void)
+{
+ struct received received = {0};
+ struct clipboard c = {.loop = yasl_loop_create(), .receive = receive, .ud = &received};
+ assert(c.loop);
+ const struct { const char *bytes; size_t len; bool valid; } cases[] = {
+ {"é字😀", sizeof("é字😀") - 1, true},
+ {"", 0, true},
+ {"\xff", 1, false},
+ {"\xf0\x9f\x98", 3, false}, /* Incomplete non-BMP character. */
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+ received = (struct received){0};
+ int fds[2];
+ assert(pipe(fds) == 0);
+ assert(start_transfer(&c, fds[0], false, false));
+ assert(write(fds[1], cases[i].bytes, cases[i].len) == (ssize_t)cases[i].len);
+ close(fds[1]);
+ for (int steps = 0; c.transfers && steps < 10; steps++)
+ assert(yasl_loop_step(c.loop, 0) >= 0);
+ assert(!c.transfers);
+ assert(received.count == (int)cases[i].valid);
+ if (cases[i].valid) {
+ assert(received.len == cases[i].len);
+ assert(memcmp(received.text, cases[i].bytes, received.len) == 0);
+ }
+ }
+ yasl_loop_destroy(c.loop);
+ return 0;
+}
diff --git a/tests/wayland/test_keyboard.c b/tests/wayland/test_keyboard.c
new file mode 100644
index 0000000..dbe0a94
--- /dev/null
+++ b/tests/wayland/test_keyboard.c
@@ -0,0 +1,128 @@
+#include "../../src/wayland/keyboard.c"
+#include <assert.h>
+
+static struct yasl_input_event next(struct wayland_surface *w, enum yasl_input_kind kind)
+{
+ struct yasl_input_event ev;
+ assert(yasl_surface_input_poll(&w->base, &ev));
+ assert(ev.kind == kind && ev.surface == &w->base);
+ return ev;
+}
+
+static void empty(struct wayland_surface *w)
+{
+ struct yasl_input_event ev;
+ assert(!yasl_surface_input_poll(&w->base, &ev));
+}
+
+static void layout(struct seat *s, const char *name, const char *variant)
+{
+ keyboard_text_reset(s);
+ if (s->xkb_state) xkb_state_unref(s->xkb_state);
+ if (s->xkb_keymap) xkb_keymap_unref(s->xkb_keymap);
+ struct xkb_rule_names names = {.layout = name, .variant = variant};
+ s->xkb_keymap = xkb_keymap_new_from_names(s->conn->xkb_ctx, &names, 0);
+ assert(s->xkb_keymap);
+ s->xkb_state = xkb_state_new(s->xkb_keymap);
+ assert(s->xkb_state);
+ s->input_mods_ready = false;
+ s->mod_alt = xkb_keymap_mod_get_index(s->xkb_keymap, XKB_MOD_NAME_ALT);
+}
+
+static void modifiers(struct seat *s, const char *name)
+{
+ xkb_mod_mask_t mask = name ? 1u << xkb_keymap_mod_get_index(s->xkb_keymap, name) : 0;
+ xkb_state_update_mask(s->xkb_state, mask, 0, 0, 0, 0, 0);
+}
+
+static void key(struct seat *s, uint32_t code, bool down)
+{
+ keyboard_listener.key(s, NULL, 1, 0, code, down ?
+ WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
+}
+
+int main(void)
+{
+ struct wayland_display d = {.base.ops = &yasl_wayland_backend};
+ d.xkb_ctx = xkb_context_new(0);
+ assert(d.xkb_ctx);
+ struct wayland_surface w = {.base.display = &d.base, .conn = &d};
+ wayland_surface_set_callbacks(&w, NULL, NULL);
+ struct seat s = {.conn = &d, .keyboard_focus = &w, .repeat_fd = -1,
+ .compose_attempted = true};
+ const char compose[] = "<dead_acute> <e> : \"é\" U00E9\n";
+ struct xkb_compose_table *table = xkb_compose_table_new_from_buffer(d.xkb_ctx,
+ compose, sizeof(compose) - 1, "C.UTF-8", XKB_COMPOSE_FORMAT_TEXT_V1, 0);
+ assert(table);
+ s.compose = xkb_compose_state_new(table, 0);
+ assert(s.compose);
+ xkb_compose_table_unref(table);
+ layout(&s, "us", NULL);
+ key(&s, KEY_A, true);
+ struct yasl_input_event ev = next(&w, YASL_INPUT_KEY);
+ assert(ev.key.key == YASL_KEY_A && ev.key.native_code == KEY_A);
+ assert(ev.key.codepoint == 'a' && ev.key.shifted == 'A' && !ev.key.mods);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "a") == 0);
+ modifiers(&s, XKB_MOD_NAME_SHIFT);
+ key(&s, KEY_A, false); /* Modifier changes cannot change physical identity. */
+ ev = next(&w, YASL_INPUT_KEY);
+ assert(ev.key.key == YASL_KEY_A && ev.key.action == YASL_KEY_RELEASE);
+ assert(ev.key.mods == YASL_MOD_SHIFT);
+ empty(&w);
+ key(&s, KEY_A, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "A") == 0);
+ modifiers(&s, XKB_MOD_NAME_CAPS);
+ key(&s, KEY_A, true);
+ assert(next(&w, YASL_INPUT_KEY).key.mods == YASL_MOD_CAPS);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "A") == 0);
+ const char *shortcuts[] = {XKB_MOD_NAME_CTRL, XKB_MOD_NAME_ALT, XKB_MOD_NAME_LOGO};
+ for (size_t i = 0; i < sizeof(shortcuts)/sizeof(shortcuts[0]); i++) {
+ modifiers(&s, shortcuts[i]);
+ key(&s, KEY_A, true);
+ next(&w, YASL_INPUT_KEY);
+ empty(&w); /* Shortcut characters never leak into committed text. */
+ }
+ layout(&s, "fr", NULL);
+ key(&s, KEY_A, true);
+ ev = next(&w, YASL_INPUT_KEY);
+ assert(ev.key.key == YASL_KEY_A && ev.key.codepoint == 'q');
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "q") == 0);
+ layout(&s, "de", NULL);
+ modifiers(&s, "Mod5"); /* AltGr is text input, not an Alt shortcut. */
+ key(&s, KEY_E, true);
+ assert(next(&w, YASL_INPUT_KEY).key.key == YASL_KEY_E);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "€") == 0);
+ layout(&s, "us", "intl");
+ key(&s, KEY_APOSTROPHE, true);
+ next(&w, YASL_INPUT_KEY);
+ empty(&w);
+ key(&s, KEY_E, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "é") == 0);
+ assert(keyboard_dispatch_key(&s, &w, KEY_E, YASL_KEY_REPEAT));
+ assert(next(&w, YASL_INPUT_KEY).key.action == YASL_KEY_REPEAT);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0);
+ key(&s, KEY_APOSTROPHE, true);
+ next(&w, YASL_INPUT_KEY);
+ keyboard_reset_keys(&s, &w);
+ key(&s, KEY_E, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0);
+ key(&s, KEY_APOSTROPHE, true);
+ next(&w, YASL_INPUT_KEY);
+ s.ime_focus = &w;
+ text_input_listener.commit_string(&s, NULL, "字");
+ text_input_listener.done(&s, NULL, 0);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "字") == 0);
+ key(&s, KEY_E, true);
+ next(&w, YASL_INPUT_KEY);
+ assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0);
+ empty(&w);
+ xkb_compose_state_unref(s.compose);
+ xkb_state_unref(s.xkb_state);
+ xkb_keymap_unref(s.xkb_keymap);
+ xkb_context_unref(d.xkb_ctx);
+ input_queue_clear(&w.input);
+ return 0;
+}
diff --git a/tests/wayland/test_tablet.c b/tests/wayland/test_tablet.c
index 449c446..f8982b0 100644
--- a/tests/wayland/test_tablet.c
+++ b/tests/wayland/test_tablet.c
@@ -50,6 +50,7 @@ int main(void)
tool_frame(&tool, NULL, 0);
ev = next(&surface);
assert(ev.kind == YASL_PTR_SCROLL && ev.device == YASL_PTR_DEV_ERASER);
+ assert(ev.scroll.x == 0 && ev.scroll.y == 1);
tablet_tool_proximity_out(&tool, NULL);
ev = next(&surface);
assert(ev.kind == YASL_PTR_LEAVE && ev.device == YASL_PTR_DEV_ERASER && !ev.pen.pressure);
@@ -57,6 +58,42 @@ int main(void)
assert(!(yasl_display_caps(&display.base) & YASL_CAP_TABLET));
display.tablet_mgr = (void *)&display; /* Capability depends on protocol availability. */
assert(yasl_display_caps(&display.base) & YASL_CAP_TABLET);
+
+ seat.pointer_focus = &surface;
+ /* A compositor may describe the same detent in three ways. value120 wins,
+ * and both axes belong to one event, without whole-line truncation. */
+ pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(10));
+ pointer_axis_discrete(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 1);
+ pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 30);
+ pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_HORIZONTAL_SCROLL, -60);
+ pointer_frame(&seat, NULL);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_SCROLL && ev.device == YASL_PTR_DEV_MOUSE);
+ assert(ev.scroll.x == -0.5 && ev.scroll.y == 0.25);
+ struct yasl_input_event unused;
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
+ pointer_frame(&seat, NULL);
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
+
+ /* Stops preserve offsets from the same frame. Repeated tiny movements
+ * must be visible immediately rather than held until a whole detent. */
+ for (int i = 0; i < 3; i++) {
+ pointer_axis_source(&seat, NULL, WL_POINTER_AXIS_SOURCE_FINGER);
+ pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(1.25));
+ pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_double(-2.5));
+ pointer_axis_stop(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL);
+ pointer_frame(&seat, NULL);
+ ev = next(&surface);
+ assert(ev.scroll.x == -0.25 && ev.scroll.y == 0.125);
+ }
+ pointer_axis_discrete(&seat, NULL, WL_POINTER_AXIS_HORIZONTAL_SCROLL, 2);
+ pointer_frame(&seat, NULL);
+ ev = next(&surface);
+ assert(ev.scroll.x == 2 && ev.scroll.y == 0);
+ pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 60);
+ seat_pointer_reset(&seat);
+ pointer_frame(&seat, NULL);
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
input_queue_clear(&surface.input);
return 0;
}
diff --git a/tests/x11/test_input.c b/tests/x11/test_input.c
new file mode 100644
index 0000000..11dca4f
--- /dev/null
+++ b/tests/x11/test_input.c
@@ -0,0 +1,123 @@
+/* An isolated real X server supplies the keymap; disabling this surface's XIC
+ * deliberately exercises text insertion when no input method is available. */
+#include "../../src/x11/window.c"
+
+static void check_selection_encoding(struct x11_display *d, struct x11_surface *s)
+{
+ Window requestor = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native),
+ 0, 0, 1, 1, 0, 0, 0);
+ Atom property = XInternAtom(d->native, "YASL_TEST_STRING", False);
+ XSelectionRequestEvent request = {.display = d->native, .requestor = requestor,
+ .selection = d->clipboard, .target = XA_STRING, .property = property};
+ yasl_display_copy(&d->base, YASL_CLIPBOARD, "café £");
+ selection_request(d, &request);
+ Atom actual;
+ int format;
+ unsigned long count, left;
+ unsigned char *data = NULL;
+ assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True,
+ AnyPropertyType, &actual, &format, &count, &left, &data) == Success);
+ assert(actual == XA_STRING && format == 8 && count == 6);
+ assert(memcmp(data, "caf\xe9 \xa3", count) == 0);
+ XFree(data);
+ yasl_display_copy(&d->base, YASL_CLIPBOARD, "字");
+ selection_request(d, &request);
+ assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True,
+ AnyPropertyType, &actual, &format, &count, &left, &data) == Success);
+ assert(actual == None); /* Never mislabel UTF-8 as Latin-1. */
+ if (data) XFree(data);
+ request.target = d->targets;
+ selection_request(d, &request);
+ assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True,
+ AnyPropertyType, &actual, &format, &count, &left, &data) == Success);
+ assert(actual == XA_ATOM && format == 32 && count == 2);
+ assert(((Atom *)data)[0] == d->targets && ((Atom *)data)[1] == d->utf8);
+ XFree(data);
+ XDestroyWindow(d->native, requestor);
+
+ /* Simulate a legacy owner returning STRING to the paste conversion window,
+ * then an empty UTF-8 owner and malformed UTF-8. */
+ const struct { Atom type; const char *bytes; size_t len; const char *expected; } cases[] = {
+ {XA_STRING, "caf\xe9 \xa3", 6, "café £"},
+ {d->utf8, "", 0, ""},
+ {d->utf8, "\xff", 1, NULL},
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+ struct paste_request *pending = calloc(1, sizeof(*pending));
+ assert(pending);
+ pending->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native),
+ 0, 0, 1, 1, 0, 0, 0);
+ pending->target = s;
+ pending->next = d->pastes;
+ d->pastes = pending;
+ XChangeProperty(d->native, pending->window, property, cases[i].type, 8,
+ PropModeReplace, (const unsigned char *)cases[i].bytes, (int)cases[i].len);
+ XSelectionEvent reply = {.requestor = pending->window, .property = property};
+ selection_notify(d, &reply);
+ struct yasl_input_event event;
+ if (cases[i].expected) {
+ assert(yasl_surface_input_poll(&s->base, &event));
+ assert(event.kind == YASL_INPUT_PASTE);
+ assert(event.paste.len == strlen(cases[i].expected));
+ assert(strcmp(event.paste.text, cases[i].expected) == 0);
+ }
+ assert(!yasl_surface_input_poll(&s->base, &event));
+ }
+}
+
+int main(void)
+{
+ struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11};
+ struct yasl_display *base = yasl_display_open(NULL, &ds);
+ assert(base);
+ struct x11_display *d = (struct x11_display *)base;
+ struct yasl_surface_spec spec = {.title = "Keyboard fallback", .size = {100, 100}};
+ struct yasl_surface *surface = yasl_surface_create_toplevel(base, &spec);
+ assert(surface);
+ struct x11_surface *s = (struct x11_surface *)surface;
+ XIC saved_ic = s->ic;
+ s->ic = NULL;
+ unsigned code = XKeysymToKeycode(d->native, XK_a);
+ int width = 0;
+ KeySym *saved = XGetKeyboardMapping(d->native, code, 1, &width);
+ assert(saved && width >= 2);
+ KeySym *symbols = calloc((size_t)width, sizeof(*symbols));
+ assert(symbols);
+ const struct { KeySym symbol, shifted; const char *text; uint32_t label; } cases[] = {
+ {XK_eacute, XK_Eacute, "é", 0xe9},
+ {0x01000416, 0x01000416, "Ж", 0x416},
+ {0x0101f600, 0x0101f600, "😀", 0x1f600},
+ };
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
+ symbols[0] = cases[i].symbol;
+ symbols[1] = cases[i].shifted;
+ XChangeKeyboardMapping(d->native, code, width, symbols, 1);
+ XSync(d->native, False);
+ dispatch(base);
+ input_queue_clear(&s->input);
+ XKeyEvent native = {.type = KeyPress, .display = d->native,
+ .window = s->window, .keycode = code};
+ key_event(s, &native, false);
+ struct yasl_input_event event;
+ assert(yasl_surface_input_poll(surface, &event));
+ assert(event.kind == YASL_INPUT_KEY && event.key.key == YASL_KEY_A);
+ assert(event.key.codepoint == cases[i].label);
+ assert(yasl_surface_input_poll(surface, &event));
+ assert(event.kind == YASL_INPUT_TEXT && event.text.len == strlen(cases[i].text));
+ assert(strcmp(event.text.text, cases[i].text) == 0);
+ assert(!yasl_surface_input_poll(surface, &event));
+ native.type = KeyRelease;
+ key_event(s, &native, false);
+ assert(yasl_surface_input_poll(surface, &event) && event.kind == YASL_INPUT_KEY);
+ assert(!yasl_surface_input_poll(surface, &event));
+ }
+ XChangeKeyboardMapping(d->native, code, width, saved, 1);
+ XSync(d->native, False);
+ free(symbols);
+ XFree(saved);
+ s->ic = saved_ic;
+ check_selection_encoding(d, s);
+ yasl_surface_destroy(surface);
+ yasl_display_close(base);
+ return 0;
+}
diff --git a/tests/x11/test_tablet.c b/tests/x11/test_tablet.c
index c35a266..ba815cb 100644
--- a/tests/x11/test_tablet.c
+++ b/tests/x11/test_tablet.c
@@ -341,6 +341,19 @@ int main(void)
xi_pointer_event(&surface, &sample);
ev = next(&surface);
assert(ev.device == YASL_PTR_DEV_MOUSE && ev.button == 2 && ev.pen.pressure == 0);
+ for (int button = 4; button <= 7; button++) {
+ sample.detail = button;
+ sample.evtype = XI_ButtonPress;
+ xi_pointer_event(&surface, &sample);
+ ev = next(&surface);
+ assert(ev.kind == YASL_PTR_SCROLL);
+ assert(ev.scroll.x == (button == 6 ? -1 : button == 7 ? 1 : 0));
+ assert(ev.scroll.y == (button == 4 ? -1 : button == 5 ? 1 : 0));
+ sample.evtype = XI_ButtonRelease;
+ xi_pointer_event(&surface, &sample);
+ struct yasl_input_event unused;
+ assert(!yasl_surface_input_poll(&surface.base, &unused));
+ }
assert(pointer_button(2) == 1 && pointer_button(8) == 3);
pointer.pressure = (struct tablet_axis){2, 1, 1, 50};
assert(pressure_value(&pointer.pressure) == 0);