Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -2701,7 +2701,21 @@ - (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent {
// Mac Catalyst host keyboard, hardware keyboard in the iOS simulator via
// Cmd-Shift-K). UIKey arrived in iOS 13.4 -- on older versions the
// responder chain falls back to the existing UITextField editing path.
//
// While a native text editor is up (editingComponent != nil) we must not
// consume any UIPress -- UIKit's text-input pipeline needs every press
// (including printable characters, which cn1MapUIKeyToKeyCode returns as
// their unicode codepoint) to reach the focused CN1UITextField /
// CN1UITextView for insertion. The original implementation swallowed
// every press that mapped to a non-zero CN1 keycode, which on iOS 13.4+
// broke hardware-keyboard typing outright and on iOS 26.x devices, where
// some on-screen keyboard interactions also surface as UIPress events,
// broke virtual-keyboard typing too -- see #5010.
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
if (editingComponent != nil) {
[super pressesBegan:presses withEvent:event];
return;
}
if (@available(iOS 13.4, *)) {
BOOL handled = NO;
NSMutableSet *passthrough = nil;
Expand Down Expand Up @@ -2732,6 +2746,10 @@ - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)eve
}

- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
if (editingComponent != nil) {
[super pressesEnded:presses withEvent:event];
return;
}
if (@available(iOS 13.4, *)) {
BOOL handled = NO;
NSMutableSet *passthrough = nil;
Expand Down Expand Up @@ -2762,6 +2780,10 @@ - (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)eve
}

- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
if (editingComponent != nil) {
[super pressesCancelled:presses withEvent:event];
return;
}
if (@available(iOS 13.4, *)) {
for (UIPress *press in presses) {
UIKey *key = press.key;
Expand Down
13 changes: 11 additions & 2 deletions scripts/input-validation-app/README.adoc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
= CN1 Input Validation App

A minimal Codename One app whose only purpose is to assert that physical
input events (tap, drag, long-press) reach Component listeners end-to-end
on iOS. Driven by XCUITest on the iOS simulator.
input events (tap, drag, long-press, keyboard) reach Component listeners
end-to-end on iOS. Driven by XCUITest on the iOS simulator.

== Why a separate pipeline?

Expand Down Expand Up @@ -44,6 +44,15 @@ the expected time.
| `addLongPressListener` fires for a press-and-hold. Routes through the
same touch chain as tap, so a recognizer that cancels touches mid-press
causes this step to time out.

| `keytype`
| Typed characters reach a CN1 `TextField.DataChangedListener`. The
driver taps the field to bring up native iOS editing, then synthesises
keystrokes via `XCUIApplication.typeText`. typeText drives the
simulator's HW-keyboard pathway, which on iOS 13.4+ surfaces as
`UIPress` events through `GLViewController` -- the path #5010 broke
by treating every printable key as "handled" and never forwarding it
to UIKit's text-input pipeline.
|===

== Log markers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public GestureSuite() {
this.steps = new GestureStep[] {
new TapStep(),
new DragStep(),
new LongPressStep()
new LongPressStep(),
new KeyTypeStep()
};
this.form = new Form("Input Validation", new BorderLayout());
this.statusLabel = new Label("Initializing");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*/
package com.codenameone.inputvalidation.gestures;

import com.codename1.ui.Container;
import com.codename1.ui.Font;
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;
import com.codename1.ui.layouts.BorderLayout;

/// Validates that keyboard input lands in a CN1 TextField end-to-end.
/// The XCUITest driver taps the field to bring up native iOS editing
/// (CN1UITextField becomes first responder), then synthesises keystrokes
/// via `XCUIApplication.typeText`. typeText drives the simulator's
/// hardware-keyboard pathway, which on iOS 13.4+ surfaces as UIPress
/// events that walk the responder chain through `GLViewController`.
///
/// Regression coverage for #5010: a pressesBegan: handler in
/// CodenameOne_GLViewController.m treated every UIPress whose UIKey
/// mapped to a non-zero CN1 keycode as consumed -- and printable
/// characters map to their unicode codepoint, which is always non-zero.
/// That swallowed every HW keystroke before UIKit could convert it into
/// insertText: on the focused CN1UITextField, and on iOS 26.x devices
/// the same path also swallowed virtual-keyboard input. The fix bypasses
/// the intercept while editingComponent != nil. This step fails to
/// receive EXPECTED_TEXT and times out if either bug ever returns.
public final class KeyTypeStep implements GestureStep {
/// XCUITest types this exact string. Kept short, lowercase, and not a
/// dictionary word so iOS auto-capitalisation / auto-correct cannot
/// silently rewrite the characters before the CN1 TextField sees them.
public static final String EXPECTED_TEXT = "cn1";

@Override
public String name() {
return "keytype";
}

@Override
public void install(Container target, Callback callback) {
// Disable predictive text in the constraint so simulated keystrokes
// land verbatim. Without this iOS auto-capitalisation can rewrite
// the first character (`Cn1` instead of `cn1`) and make the
// assertion brittle across keyboard configurations.
final TextField field = new TextField("", "Type " + EXPECTED_TEXT + " here",
EXPECTED_TEXT.length() + 8,
TextArea.ANY | TextArea.NON_PREDICTIVE);
field.setName("cn1iv-keytype-target");
// Match TapStep / LongPressStep tap-target sizing so the XCUITest
// driver can use the same (0.5, 0.5) coordinate to focus the
// field on every iPhone size class on the CI runner. A NORTH
// placement put the field above where the existing steps tap
// and the driver missed it -- see #5010 CI failure.
field.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE));
field.getAllStyles().setPadding(48, 48, 48, 48);
field.getAllStyles().setMargin(48, 48, 48, 48);
final boolean[] fired = {false};
field.addDataChangedListener((type, index) -> {
String text = field.getText();
if (!fired[0] && text != null && text.contains(EXPECTED_TEXT)) {
fired[0] = true;
callback.onDetected("text=" + text);
}
});
target.add(BorderLayout.CENTER, field);
}
}
2 changes: 2 additions & 0 deletions scripts/input-validation-app/drivers/run-ios.sh
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ REQUIRED_EVENTS=(
"CN1IV:EVENT:drag"
"CN1IV:READY:longpress"
"CN1IV:EVENT:longpress"
"CN1IV:READY:keytype"
"CN1IV:EVENT:keytype"
"CN1IV:SUITE:FINISHED"
)
FAILED=0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ final class InputValidationUITests: XCTestCase {

try driveLongPress(app: app)
Thread.sleep(forTimeInterval: stepDelaySeconds)

try driveKeyType(app: app)
Thread.sleep(forTimeInterval: stepDelaySeconds)
}

private func driveTap(app: XCUIApplication) throws {
Expand All @@ -71,4 +74,21 @@ final class InputValidationUITests: XCTestCase {
let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
center.press(forDuration: 1.5)
}

private func driveKeyType(app: XCUIApplication) throws {
// KeyTypeStep places its TextField in BorderLayout.CENTER with
// generous padding/margin, matching the layout TapStep and
// LongPressStep use so a single screen-center tap focuses it on
// every iPhone size class on the CI runner. Wait briefly after
// the tap for CN1's editStringAtImpl to install the native
// CN1UITextField and animate the keyboard in -- typeText raises
// "Neither element nor any descendant has keyboard focus" if no
// first responder accepts input yet -- then type. typeText
// synthesises HW-keyboard UIPress events that walk through
// GLViewController, exactly the path #5010 broke.
let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
center.tap()
Thread.sleep(forTimeInterval: 2.0)
app.typeText("cn1")
}
}
Loading