diff --git a/.github/workflows/input-validation.yml b/.github/workflows/input-validation.yml
new file mode 100644
index 0000000000..d09ab8e7c8
--- /dev/null
+++ b/.github/workflows/input-validation.yml
@@ -0,0 +1,202 @@
+name: Input validation gesture suite
+
+# Fast-running iOS suite that asserts physical taps / drags / long-presses
+# reach Component listeners end-to-end. Built to catch regressions in the
+# input chain (the class of bug PR #5003 fixed -- a window-level
+# UITapGestureRecognizer eating every tap was invisible to the existing
+# screenshot-only tests because none of them actually depended on a touch
+# event firing).
+#
+# Only iOS for now. The JavaScript port wires its pointer listeners from
+# inside a Web Worker via a host-bridge proxy; synthetic events from the
+# test driver don't traverse that proxy the way real OS events would, so
+# end-to-end input validation on JS needs port-side work first. See
+# scripts/input-validation-app/README.adoc for the "not yet covered" list.
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/input-validation.yml'
+ - 'scripts/input-validation-app/**'
+ - 'scripts/build-ios-app.sh'
+ - 'CodenameOne/src/com/codename1/ui/Component.java'
+ - 'CodenameOne/src/com/codename1/ui/Form.java'
+ - 'CodenameOne/src/com/codename1/ui/Button.java'
+ - 'Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m'
+ - 'Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m'
+ push:
+ branches: [ master ]
+ paths:
+ - '.github/workflows/input-validation.yml'
+ - 'scripts/input-validation-app/**'
+ - 'scripts/build-ios-app.sh'
+ workflow_dispatch: {}
+
+jobs:
+ build-port:
+ # Reused by `ios` so the iOS port .jar / native sources are available in
+ # the cache the build-ios-app step relies on. Same reusable workflow that
+ # scripts-ios.yml depends on -- piggybacking on its cache key.
+ uses: ./.github/workflows/_build-ios-port.yml
+
+ ios:
+ needs: build-port
+ permissions:
+ contents: read
+ runs-on: macos-15
+ timeout-minutes: 35
+ concurrency:
+ group: mac-ci-${{ github.workflow }}-ios-${{ github.ref_name }}
+ cancel-in-progress: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
+ GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
+ CN1_APP_DIR: scripts/input-validation-app
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Cache CocoaPods and user gems
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gem
+ ~/Library/Caches/CocoaPods
+ ~/.cocoapods/repos
+ key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }}
+ restore-keys: |
+ ${{ runner.os }}-pods-v1-
+
+ - name: Ensure CocoaPods tooling
+ run: |
+ mkdir -p ~/.codenameone
+ cp maven/UpdateCodenameOne.jar ~/.codenameone/
+ set -euo pipefail
+ GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')"
+ export PATH="$GEM_USER_DIR/bin:$PATH"
+ if ! command -v pod >/dev/null 2>&1; then
+ gem install cocoapods xcodeproj --no-document --user-install
+ fi
+ pod --version
+
+ - name: Compute setup-workspace hash
+ id: setup_hash
+ run: echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT"
+
+ - name: Compute CN1 source hash
+ id: src_hash
+ run: |
+ set -euo pipefail
+ SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
+ -type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
+ | sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
+ POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
+ | sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
+ SCRIPT_HASH=$(shasum -a 256 \
+ scripts/setup-workspace.sh \
+ scripts/build-ios-port.sh \
+ scripts/build-native-themes.sh \
+ .github/workflows/_build-ios-port.yml \
+ | shasum -a 256 | awk '{print $1}')
+ echo "hash=${SRC_HASH:0:16}-${POM_HASH:0:16}-${SCRIPT_HASH:0:16}" >> "$GITHUB_OUTPUT"
+
+ - name: Set TMPDIR
+ run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV
+
+ - name: Cache codenameone-tools
+ uses: actions/cache@v4
+ with:
+ path: ${{ runner.temp }}/codenameone-tools
+ key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }}
+ restore-keys: |
+ ${{ runner.os }}-cn1-tools-
+
+ - name: Cache Maven repository
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-m2-
+
+ - name: Restore cn1-binaries cache
+ uses: actions/cache@v4
+ with:
+ path: ../cn1-binaries
+ key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }}
+ restore-keys: |
+ cn1-binaries-${{ runner.os }}-
+
+ - name: Restore built CN1 + iOS port artifacts
+ uses: actions/cache/restore@v4
+ with:
+ path: |
+ ~/.m2/repository/com/codenameone
+ Themes
+ Ports/iOSPort/nativeSources
+ key: cn1-built-${{ runner.os }}-${{ steps.src_hash.outputs.hash }}
+ fail-on-cache-miss: true
+
+ - name: Install XcodeGen
+ run: brew install xcodegen
+
+ - name: Build CN1 input-validation iOS Xcode project
+ id: build-ios-app
+ run: ./scripts/build-ios-app.sh -q -DskipTests
+ timeout-minutes: 25
+
+ - name: Build .app bundle for simulator
+ id: build-app-bundle
+ run: |
+ set -euo pipefail
+ WORKSPACE='${{ steps.build-ios-app.outputs.workspace }}'
+ SCHEME='${{ steps.build-ios-app.outputs.scheme }}'
+ DERIVED_DATA="${{ runner.temp }}/cn1iv-dd"
+ rm -rf "$DERIVED_DATA"
+ # Force ARCHS to match the host. IOSSimd.m uses ARM NEON intrinsics
+ # under #if defined(__aarch64__) -- without ONLY_ACTIVE_ARCH=YES the
+ # generic simulator destination triggers a universal build whose
+ # x86_64 slice fails to compile. Matches what run-ios-ui-tests.sh
+ # does for the hellocodenameone build.
+ HOST_ARCH="$(uname -m)"
+ case "$HOST_ARCH" in
+ arm64|x86_64) BUILD_ARCH="$HOST_ARCH" ;;
+ *) BUILD_ARCH=arm64 ;;
+ esac
+ XCB_CONTAINER_FLAG="-workspace"
+ if [[ "$WORKSPACE" != *.xcworkspace ]]; then
+ XCB_CONTAINER_FLAG="-project"
+ fi
+ xcodebuild $XCB_CONTAINER_FLAG "$WORKSPACE" -scheme "$SCHEME" \
+ -sdk iphonesimulator \
+ -configuration Debug \
+ -destination 'generic/platform=iOS Simulator' \
+ -derivedDataPath "$DERIVED_DATA" \
+ ARCHS=$BUILD_ARCH ONLY_ACTIVE_ARCH=YES \
+ EXCLUDED_ARCHS="armv7 armv7s" \
+ CODE_SIGNING_ALLOWED=NO build
+ APP_BUNDLE="$(find "$DERIVED_DATA/Build/Products" -maxdepth 3 -type d -name "${SCHEME}.app" | head -n 1)"
+ if [ -z "$APP_BUNDLE" ]; then
+ echo "Failed to locate .app bundle under $DERIVED_DATA" >&2
+ find "$DERIVED_DATA/Build/Products" -maxdepth 4 -type d -print >&2 || true
+ exit 1
+ fi
+ echo "app_bundle=$APP_BUNDLE" >> "$GITHUB_OUTPUT"
+ timeout-minutes: 15
+
+ - name: Drive gestures via XCUITest
+ env:
+ ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/input-validation-ios
+ run: |
+ mkdir -p "$ARTIFACTS_DIR"
+ ./scripts/input-validation-app/drivers/run-ios.sh \
+ '${{ steps.build-app-bundle.outputs.app_bundle }}'
+ timeout-minutes: 15
+
+ - name: Upload iOS artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: input-validation-ios
+ path: artifacts/input-validation-ios
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/scripts/build-ios-app.sh b/scripts/build-ios-app.sh
index 2bf7da8baf..469cd84188 100755
--- a/scripts/build-ios-app.sh
+++ b/scripts/build-ios-app.sh
@@ -78,12 +78,21 @@ if [ -n "${IOS_DEPENDENCY_ARGS:-}" ]; then
bia_log "Applying extra iOS build args: ${IOS_DEPENDENCY_ARGS}"
fi
-APP_DIR="scripts/hellocodenameone"
+APP_DIR="${CN1_APP_DIR:-scripts/hellocodenameone}"
+
+# Derive the iOS project / scheme name from the app's own CN1 settings so
+# this script can build any CN1 app, not just hellocodenameone.
+CN1_SETTINGS_FILE="$REPO_ROOT/$APP_DIR/common/codenameone_settings.properties"
+if [ -f "$CN1_SETTINGS_FILE" ]; then
+ MAIN_NAME_FROM_SETTINGS="$(awk -F= '/^codename1.mainName=/{print $2; exit}' "$CN1_SETTINGS_FILE" | tr -d '\r')"
+fi
+APP_MAIN_NAME="${CN1_APP_MAIN_NAME:-${MAIN_NAME_FROM_SETTINGS:-HelloCodenameOne}}"
+bia_log "Using APP_DIR=$APP_DIR APP_MAIN_NAME=$APP_MAIN_NAME"
xcodebuild -version
bia_log "Building iOS Xcode project using Codename One port"
-cd $APP_DIR
+cd "$REPO_ROOT/$APP_DIR"
VM_START=$(date +%s)
ARTIFACTS_DIR="${ARTIFACTS_DIR:-$REPO_ROOT/artifacts}"
@@ -175,11 +184,11 @@ stage_bytecode_translator_sources() {
bia_log "Created archive $zip_file"
}
-bia_log "Running HelloCodenameOne Maven build with JAVA_HOME=$JAVA17_HOME"
+bia_log "Running $APP_MAIN_NAME Maven build with JAVA_HOME=$JAVA17_HOME"
(
export JAVA_HOME="$JAVA17_HOME"
export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$BASE_PATH"
- MVN_IOS_LOG="$ARTIFACTS_DIR/hellocn1-ios-build.log"
+ MVN_IOS_LOG="$ARTIFACTS_DIR/cn1-ios-build.log"
MVN_CMD=(
./mvnw package
-DskipTests
@@ -211,7 +220,7 @@ bia_log "Running HelloCodenameOne Maven build with JAVA_HOME=$JAVA17_HOME"
)
VM_END=$(date +%s)
VM_TIME=$((VM_END - VM_START))
-cd ../..
+cd "$REPO_ROOT"
echo "$VM_TIME" > "$ARTIFACTS_DIR/vm_time.txt"
bia_log "VM translation time: ${VM_TIME}s (saved to $ARTIFACTS_DIR/vm_time.txt)"
@@ -269,22 +278,22 @@ else
bia_log "Podfile not found in generated project; skipping pod install"
fi
-WORKSPACE_XML='
+WORKSPACE_XML="
+ version = \"1.0\">
+ location = \"group:${APP_MAIN_NAME}.xcodeproj\">
-'
-if [ ! -d "$PROJECT_DIR/HelloCodenameOne.xcworkspace" ] && [ -d "$PROJECT_DIR/HelloCodenameOne.xcodeproj" ]; then
+"
+if [ ! -d "$PROJECT_DIR/${APP_MAIN_NAME}.xcworkspace" ] && [ -d "$PROJECT_DIR/${APP_MAIN_NAME}.xcodeproj" ]; then
bia_log "Creating fallback xcworkspace for generated Xcode project"
- mkdir -p "$PROJECT_DIR/HelloCodenameOne.xcworkspace"
- printf '%s\n' "$WORKSPACE_XML" > "$PROJECT_DIR/HelloCodenameOne.xcworkspace/contents.xcworkspacedata"
+ mkdir -p "$PROJECT_DIR/${APP_MAIN_NAME}.xcworkspace"
+ printf '%s\n' "$WORKSPACE_XML" > "$PROJECT_DIR/${APP_MAIN_NAME}.xcworkspace/contents.xcworkspacedata"
fi
-if [ -d "$PROJECT_DIR/HelloCodenameOne.xcodeproj" ]; then
+if [ -d "$PROJECT_DIR/${APP_MAIN_NAME}.xcodeproj" ]; then
bia_log "Ensuring shared Xcode scheme exists"
- "$REPO_ROOT/scripts/ios/create-shared-scheme.py" "$PROJECT_DIR" HelloCodenameOne
+ "$REPO_ROOT/scripts/ios/create-shared-scheme.py" "$PROJECT_DIR" "$APP_MAIN_NAME"
fi
# Locate workspace or project for the next step
@@ -315,11 +324,11 @@ bia_log "Found Xcode entrypoint: $WORKSPACE"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
{
echo "workspace=$WORKSPACE"
- echo "scheme=HelloCodenameOne"
+ echo "scheme=$APP_MAIN_NAME"
} >> "$GITHUB_OUTPUT"
fi
-bia_log "Emitted outputs -> workspace=$WORKSPACE, scheme=HelloCodenameOne"
+bia_log "Emitted outputs -> workspace=$WORKSPACE, scheme=$APP_MAIN_NAME"
# (Optional) dump xcodebuild -list for debugging
ARTIFACTS_DIR="${ARTIFACTS_DIR:-$REPO_ROOT/artifacts}"
diff --git a/scripts/input-validation-app/.mvn/jvm.config b/scripts/input-validation-app/.mvn/jvm.config
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/scripts/input-validation-app/.mvn/wrapper/MavenWrapperDownloader.java b/scripts/input-validation-app/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000000..b901097f2d
--- /dev/null
+++ b/scripts/input-validation-app/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/scripts/input-validation-app/.mvn/wrapper/maven-wrapper.properties b/scripts/input-validation-app/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000000..642d572ce9
--- /dev/null
+++ b/scripts/input-validation-app/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/scripts/input-validation-app/README.adoc b/scripts/input-validation-app/README.adoc
new file mode 100644
index 0000000000..c27929fd81
--- /dev/null
+++ b/scripts/input-validation-app/README.adoc
@@ -0,0 +1,114 @@
+= 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.
+
+== Why a separate pipeline?
+
+The existing `scripts/hellocodenameone` test suite builds every form
+programmatically and emits screenshots from `runTest()` without ever
+depending on a touch event reaching `Component.pointerPressed`. That
+gap let three independent input-chain regressions ship on iOS 26 (see
+https://github.com/codenameone/CodenameOne/pull/5003): a window-level
+`UITapGestureRecognizer` ate every tap, a hover recognizer cancelled
+touches on the simulator, and a class-literal comparison NPE fired from
+the background painter on every form show.
+
+This pipeline closes that gap by making input flow the only thing the
+app does. There is no theme, no resource bundle, no screenshot
+comparison -- the assertion is the stream of `CN1IV:EVENT:*` log lines
+emitted when each gesture fires.
+
+== Suite
+
+Each step waits up to 8 seconds for its expected event, then auto-advances
+on success or timeout. The XCUITest driver issues the actual OS input at
+the expected time.
+
+[cols="1,3"]
+|===
+| Step | What it asserts
+
+| `tap`
+| `Button.actionPerformed` fires for a single tap. The PR #5003
+ regression.
+
+| `drag`
+| `pointerDragged` is dispatched continuously between `pointerPressed`
+ and `pointerReleased`, with at least 3 intermediate samples. Catches
+ both the iOS 26 hover-recognizer regression and the empty-pointer-array
+ NPE fixed in 2fef7187.
+
+| `longpress`
+| `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.
+|===
+
+== Log markers
+
+Everything the driver cares about is a single line on stdout:
+
+----
+CN1IV:SUITE:STARTED platform= w= h=
+CN1IV:READY: # step is armed and waiting for input
+CN1IV:EVENT:: # input arrived; advancing to next step
+CN1IV:TIMEOUT: # 8s elapsed without input -- step failed
+CN1IV:SUITE:FINISHED
+----
+
+A successful run contains one `READY:` + one `EVENT:` per step and no
+`TIMEOUT:` lines. The driver greps for these and fails non-zero on any miss.
+
+== Layout
+
+----
+scripts/input-validation-app/
+├── pom.xml # parent Maven project
+├── common/ # CN1 Lifecycle + gesture screens
+├── ios/ # CN1 iOS module (codename1.platform=ios)
+├── ios-tests/ # XCUITest target (XcodeGen-managed)
+│ ├── HostStub/ # minimal UIApplication host so the test
+│ │ # runner has somewhere to attach -- the
+│ │ # actual tests target the CN1 app by bundle id
+│ └── Sources/ # XCUITest Swift code
+└── drivers/
+ └── run-ios.sh # boot sim, install, drive gestures, assert log
+----
+
+== Running locally
+
+----
+# 1. Build the CN1 iOS .app bundle.
+cd scripts/input-validation-app
+mvn -P ios package
+APP_BUNDLE=$(find ios/target -name '*.app' -type d | head -n 1)
+
+# 2. Drive the gesture suite on whatever iPhone simulator is available
+# (override with CN1IV_DEVICE_NAME / CN1IV_DEVICE_RUNTIME).
+brew install xcodegen # one-time
+./drivers/run-ios.sh "$APP_BUNDLE"
+----
+
+== CI
+
+`.github/workflows/input-validation.yml` runs the iOS job on every PR. It
+reuses the `_build-ios-port.yml` reusable workflow so it shares the same
+port-build cache as the screenshot-based scripts-ios pipeline.
+
+The build script `scripts/build-ios-app.sh` accepts a
+`CN1_APP_DIR=scripts/input-validation-app` override so the same script
+can build either app; the default stays pointed at hellocodenameone so
+the existing CI keeps passing.
+
+== Not yet covered (follow-up PRs)
+
+* JavaScript port. Synthetic DOM events reach main-thread listeners but
+ the CN1 JS port registers its pointer listeners through a host-bridge
+ proxy from inside a Web Worker; synthetic events don't traverse that
+ proxy the way real OS events do. End-to-end input validation here
+ needs port-side changes that don't belong in this PR.
+* Android (UIAutomator) on Linux.
+* JavaSE / desktop (`java.awt.Robot`).
+* Status-bar tap-to-top, soft-keyboard show/dismiss, multi-touch.
diff --git a/scripts/input-validation-app/common/codenameone_settings.properties b/scripts/input-validation-app/common/codenameone_settings.properties
new file mode 100644
index 0000000000..4973a52e4a
--- /dev/null
+++ b/scripts/input-validation-app/common/codenameone_settings.properties
@@ -0,0 +1,27 @@
+codename1.android.keystore=
+codename1.android.keystoreAlias=
+codename1.android.keystorePassword=
+codename1.arg.android.useAndroidX=true
+codename1.arg.ios.newStorageLocation=true
+codename1.arg.ios.uiscene=true
+codename1.arg.java.version=17
+codename1.cssTheme=false
+codename1.displayName=CN1InputValidation
+codename1.icon=icon.png
+codename1.ios.appid=
+codename1.ios.certificate=
+codename1.ios.certificatePassword=
+codename1.ios.debug.certificate=
+codename1.ios.debug.certificatePassword=
+codename1.ios.debug.provision=
+codename1.ios.provision=
+codename1.ios.release.certificate=
+codename1.ios.release.certificatePassword=
+codename1.ios.release.provision=
+codename1.kotlin=false
+codename1.languageLevel=5
+codename1.mainName=InputValidationApp
+codename1.packageName=com.codenameone.inputvalidation
+codename1.secondaryTitle=CN1 Input Validation
+codename1.vendor=CodenameOne
+codename1.version=1.0
diff --git a/scripts/input-validation-app/common/icon.png b/scripts/input-validation-app/common/icon.png
new file mode 100644
index 0000000000..1f4fa5dd25
Binary files /dev/null and b/scripts/input-validation-app/common/icon.png differ
diff --git a/scripts/input-validation-app/common/pom.xml b/scripts/input-validation-app/common/pom.xml
new file mode 100644
index 0000000000..8f37ff5f64
--- /dev/null
+++ b/scripts/input-validation-app/common/pom.xml
@@ -0,0 +1,23 @@
+
+
+ 4.0.0
+
+ com.codenameone.examples.inputvalidation
+ cn1-input-validation
+ 1.0-SNAPSHOT
+
+ com.codenameone.examples.inputvalidation
+ cn1-input-validation-common
+ 1.0-SNAPSHOT
+ jar
+
+
+
+ com.codenameone
+ codenameone-core
+ provided
+
+
+
diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java
new file mode 100644
index 0000000000..f52515a4f2
--- /dev/null
+++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+import com.codename1.system.Lifecycle;
+import com.codenameone.inputvalidation.gestures.GestureSuite;
+
+/// Lifecycle entry point for the input-validation CN1 app. The whole app does
+/// one thing: it runs `GestureSuite` once and exits. No theme, no resources,
+/// no asset bundle -- by design, so a regression in input handling can never
+/// hide behind a missing texture, a slow startup, or a stale screenshot
+/// baseline.
+public class InputValidationApp extends Lifecycle {
+ @Override
+ public void runApp() {
+ new Thread(() -> new GestureSuite().start(), "CN1IV-Suite").start();
+ }
+}
diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/DragStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/DragStep.java
new file mode 100644
index 0000000000..c350251f89
--- /dev/null
+++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/DragStep.java
@@ -0,0 +1,117 @@
+/*
+ * 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.CN;
+import com.codename1.ui.Component;
+import com.codename1.ui.Container;
+import com.codename1.ui.Font;
+import com.codename1.ui.Form;
+import com.codename1.ui.Graphics;
+import com.codename1.ui.Label;
+import com.codename1.ui.events.ActionListener;
+import com.codename1.ui.layouts.BorderLayout;
+
+/// Validates that pointerDragged is dispatched continuously between
+/// pointerPressed and pointerReleased. Covers (a) the iOS 26 hover-recognizer
+/// regression in PR #5003 (which cancelled the drag mid-stream) and (b) the
+/// empty-array NPE fix in 2fef7187 (`pointerDragged guard against empty pointer arrays`).
+/// The detector requires at least DRAG_MIN_SAMPLES intermediate samples so a
+/// recognizer that fires only the first or last point still fails.
+public final class DragStep implements GestureStep {
+ private static final int DRAG_MIN_SAMPLES = 3;
+
+ @Override
+ public String name() {
+ return "drag";
+ }
+
+ @Override
+ public void install(Container target, Callback callback) {
+ final DragSurface surface = new DragSurface();
+ surface.setName("cn1iv-drag-target");
+ Label hint = new Label("Drag horizontally across this area");
+ Container col = new Container(new BorderLayout());
+ col.add(BorderLayout.NORTH, hint);
+ col.add(BorderLayout.CENTER, surface);
+ target.add(BorderLayout.CENTER, col);
+
+ final int[] samples = {0};
+ final int[] firstXY = {Integer.MIN_VALUE, Integer.MIN_VALUE};
+ final int[] lastXY = {Integer.MIN_VALUE, Integer.MIN_VALUE};
+ final boolean[] fired = {false};
+
+ ActionListener dragListener = evt -> {
+ if (firstXY[0] == Integer.MIN_VALUE) {
+ firstXY[0] = evt.getX();
+ firstXY[1] = evt.getY();
+ }
+ lastXY[0] = evt.getX();
+ lastXY[1] = evt.getY();
+ samples[0]++;
+ surface.update(evt.getX(), evt.getY(), samples[0]);
+ };
+ ActionListener releaseListener = evt -> {
+ if (fired[0]) {
+ return;
+ }
+ if (samples[0] >= DRAG_MIN_SAMPLES) {
+ fired[0] = true;
+ callback.onDetected("samples=" + samples[0]
+ + ",from=" + firstXY[0] + "x" + firstXY[1]
+ + ",to=" + lastXY[0] + "x" + lastXY[1]);
+ }
+ };
+
+ // Form-level listeners catch every drag sample regardless of which child
+ // happens to be under the finger.
+ Form parent = CN.getCurrentForm();
+ if (parent != null) {
+ parent.addPointerDraggedListener(dragListener);
+ parent.addPointerReleasedListener(releaseListener);
+ }
+ }
+
+ private static final class DragSurface extends Component {
+ private int lastX = -1;
+ private int lastY = -1;
+ private int samples;
+
+ DragSurface() {
+ getAllStyles().setBgColor(0x1f2937);
+ getAllStyles().setBgTransparency(255);
+ getAllStyles().setFgColor(0xfbbf24);
+ getAllStyles().setMargin(16, 16, 16, 16);
+ getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_LARGE));
+ }
+
+ void update(int absX, int absY, int sampleCount) {
+ this.lastX = absX - getAbsoluteX();
+ this.lastY = absY - getAbsoluteY();
+ this.samples = sampleCount;
+ repaint();
+ }
+
+ @Override
+ protected com.codename1.ui.geom.Dimension calcPreferredSize() {
+ return new com.codename1.ui.geom.Dimension(
+ CN.convertToPixels(60f),
+ CN.convertToPixels(40f));
+ }
+
+ @Override
+ public void paint(Graphics g) {
+ g.setColor(0x1f2937);
+ g.fillRect(getX(), getY(), getWidth(), getHeight());
+ if (this.lastX >= 0) {
+ g.setColor(0xfbbf24);
+ int r = CN.convertToPixels(3f);
+ g.fillArc(getX() + this.lastX - r, getY() + this.lastY - r, r * 2, r * 2, 0, 360);
+ }
+ g.setColor(0xfbbf24);
+ g.drawString("samples=" + this.samples, getX() + 8, getY() + 8);
+ }
+ }
+}
diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureStep.java
new file mode 100644
index 0000000000..24c68c8256
--- /dev/null
+++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureStep.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+/// One gesture under test. {@link #install} populates the supplied target
+/// container with whatever UI it needs and arms its detection logic. When the
+/// gesture fires, the step calls {@link Callback#onDetected(String)} exactly
+/// once with optional details (e.g. sample count for a drag).
+public interface GestureStep {
+ String name();
+
+ void install(Container target, Callback callback);
+
+ interface Callback {
+ void onDetected(String details);
+ }
+}
diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java
new file mode 100644
index 0000000000..1b58d95cef
--- /dev/null
+++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ */
+package com.codenameone.inputvalidation.gestures;
+
+import com.codename1.ui.CN;
+import com.codename1.ui.Display;
+import com.codename1.ui.Form;
+import com.codename1.ui.Label;
+import com.codename1.ui.Container;
+import com.codename1.ui.layouts.BorderLayout;
+import com.codename1.ui.layouts.BoxLayout;
+import com.codename1.ui.util.UITimer;
+
+/// Drives a fixed sequence of input-event tests on a single Form. Each step waits
+/// for its expected gesture (tap, drag, long-press) and logs structured CN1IV:*
+/// markers that the platform driver script asserts against. The state machine
+/// auto-advances on either success or timeout so a broken gesture fails fast
+/// without blocking the rest of the suite.
+public final class GestureSuite {
+ private static final long DEFAULT_STEP_TIMEOUT_MS = 8000L;
+ private static final long SUITE_EXIT_DELAY_MS = 1500L;
+
+ private final GestureStep[] steps;
+ private final Form form;
+ private final Label statusLabel;
+ private final Container targetArea;
+ private int index = -1;
+ private UITimer activeTimeout;
+
+ public GestureSuite() {
+ this.steps = new GestureStep[] {
+ new TapStep(),
+ new DragStep(),
+ new LongPressStep()
+ };
+ this.form = new Form("Input Validation", new BorderLayout());
+ this.statusLabel = new Label("Initializing");
+ this.statusLabel.setName("cn1iv-status");
+ Container top = new Container(BoxLayout.y());
+ top.add(this.statusLabel);
+ this.targetArea = new Container(new BorderLayout());
+ this.targetArea.setName("cn1iv-target");
+ this.form.add(BorderLayout.NORTH, top);
+ this.form.add(BorderLayout.CENTER, this.targetArea);
+ }
+
+ public void start() {
+ log("CN1IV:SUITE:STARTED platform=" + CN.getPlatformName()
+ + " w=" + Display.getInstance().getDisplayWidth()
+ + " h=" + Display.getInstance().getDisplayHeight());
+ this.form.show();
+ CN.callSerially(this::advance);
+ }
+
+ private void advance() {
+ cancelTimeout();
+ this.index++;
+ if (this.index >= this.steps.length) {
+ finishSuite();
+ return;
+ }
+ final GestureStep step = this.steps[this.index];
+ this.statusLabel.setText("Step " + (this.index + 1) + "/" + this.steps.length
+ + ": " + step.name());
+ this.targetArea.removeAll();
+ step.install(this.targetArea, new GestureStep.Callback() {
+ @Override
+ public void onDetected(String details) {
+ log("CN1IV:EVENT:" + step.name() + (details == null ? "" : ":" + details));
+ CN.callSerially(GestureSuite.this::advance);
+ }
+ });
+ this.targetArea.revalidate();
+ log("CN1IV:READY:" + step.name());
+ this.activeTimeout = UITimer.timer((int) DEFAULT_STEP_TIMEOUT_MS, false, this.form, () -> {
+ log("CN1IV:TIMEOUT:" + step.name());
+ advance();
+ });
+ }
+
+ private void cancelTimeout() {
+ if (this.activeTimeout != null) {
+ this.activeTimeout.cancel();
+ this.activeTimeout = null;
+ }
+ }
+
+ private void finishSuite() {
+ log("CN1IV:SUITE:FINISHED");
+ UITimer.timer((int) SUITE_EXIT_DELAY_MS, false, this.form, () -> {
+ try {
+ Display.getInstance().exitApplication();
+ } catch (Throwable ignored) {
+ }
+ });
+ }
+
+ private static void log(String line) {
+ System.out.println(line);
+ }
+}
diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/LongPressStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/LongPressStep.java
new file mode 100644
index 0000000000..0ce29c6cfa
--- /dev/null
+++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/LongPressStep.java
@@ -0,0 +1,40 @@
+/*
+ * 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.Button;
+import com.codename1.ui.Container;
+import com.codename1.ui.Font;
+import com.codename1.ui.layouts.BorderLayout;
+
+/// Validates that addLongPressListener fires for a press-and-hold gesture.
+/// Long-press uses the same touch chain as tap on iOS; if a window-level
+/// recognizer cancels touches mid-press (the PR #5003 path), the long-press
+/// listener never fires and this step times out.
+public final class LongPressStep implements GestureStep {
+ @Override
+ public String name() {
+ return "longpress";
+ }
+
+ @Override
+ public void install(Container target, Callback callback) {
+ Button btn = new Button("Long-press me");
+ btn.setName("cn1iv-longpress-target");
+ btn.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE));
+ btn.getAllStyles().setPadding(48, 48, 48, 48);
+ btn.getAllStyles().setMargin(48, 48, 48, 48);
+ btn.getAllStyles().setBgColor(0x16a34a);
+ btn.getAllStyles().setBgTransparency(255);
+ btn.getAllStyles().setFgColor(0xffffff);
+ final long[] pressedAt = {0L};
+ btn.addPointerPressedListener(evt -> pressedAt[0] = System.currentTimeMillis());
+ btn.addLongPressListener(evt -> {
+ long elapsed = pressedAt[0] == 0L ? -1 : (System.currentTimeMillis() - pressedAt[0]);
+ callback.onDetected("durMs=" + elapsed);
+ });
+ target.add(BorderLayout.CENTER, btn);
+ }
+}
diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/TapStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/TapStep.java
new file mode 100644
index 0000000000..cca4892ade
--- /dev/null
+++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/TapStep.java
@@ -0,0 +1,35 @@
+/*
+ * 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.Button;
+import com.codename1.ui.Container;
+import com.codename1.ui.Font;
+import com.codename1.ui.layouts.BorderLayout;
+
+/// Validates that a single tap dispatches Button.actionPerformed end-to-end.
+/// This is the regression PR #5003 fixed -- a window-level UITapGestureRecognizer
+/// on iOS 26 was eating the touch before CN1TapGestureRecognizer saw it, so
+/// every button tap silently did nothing.
+public final class TapStep implements GestureStep {
+ @Override
+ public String name() {
+ return "tap";
+ }
+
+ @Override
+ public void install(Container target, Callback callback) {
+ Button btn = new Button("Tap me");
+ btn.setName("cn1iv-tap-target");
+ btn.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE));
+ btn.getAllStyles().setPadding(48, 48, 48, 48);
+ btn.getAllStyles().setMargin(48, 48, 48, 48);
+ btn.getAllStyles().setBgColor(0x2563eb);
+ btn.getAllStyles().setBgTransparency(255);
+ btn.getAllStyles().setFgColor(0xffffff);
+ btn.addActionListener(evt -> callback.onDetected("x=" + evt.getX() + ",y=" + evt.getY()));
+ target.add(BorderLayout.CENTER, btn);
+ }
+}
diff --git a/scripts/input-validation-app/drivers/run-ios.sh b/scripts/input-validation-app/drivers/run-ios.sh
new file mode 100755
index 0000000000..22a0ec14af
--- /dev/null
+++ b/scripts/input-validation-app/drivers/run-ios.sh
@@ -0,0 +1,218 @@
+#!/usr/bin/env bash
+# Drive the CN1 input-validation app through tap / drag / long-press on an
+# iOS simulator and assert the expected CN1IV:EVENT log lines appear.
+#
+# Usage:
+# run-ios.sh
+#
+# The .app bundle is produced by `mvn -P ios package` against the parent POM
+# in scripts/input-validation-app and the cn1-builder build server (or local
+# iOS build chain). This script is intentionally lean -- no screenshot
+# decoding, no chunked Base64, no comparison report. The only thing it cares
+# about is whether the OS-level taps reached Component listeners.
+set -euo pipefail
+
+iv_log() { echo "[run-ios] $1"; }
+
+if [ $# -lt 1 ]; then
+ iv_log "Usage: $0 " >&2
+ exit 2
+fi
+
+APP_BUNDLE="$1"
+if [ ! -d "$APP_BUNDLE" ]; then
+ iv_log "App bundle not found: $APP_BUNDLE" >&2
+ exit 3
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+TESTS_DIR="$APP_DIR/ios-tests"
+ARTIFACTS_DIR="${ARTIFACTS_DIR:-${GITHUB_WORKSPACE:-$APP_DIR}/artifacts/input-validation-ios}"
+mkdir -p "$ARTIFACTS_DIR"
+LOG_FILE="$ARTIFACTS_DIR/device.log"
+XCODEBUILD_LOG="$ARTIFACTS_DIR/xcodebuild-test.log"
+
+if ! command -v xcrun >/dev/null 2>&1; then iv_log "xcrun not on PATH" >&2; exit 3; fi
+if ! command -v xcodebuild >/dev/null 2>&1; then iv_log "xcodebuild not on PATH" >&2; exit 3; fi
+if ! command -v xcodegen >/dev/null 2>&1; then
+ iv_log "xcodegen not on PATH. Install with: brew install xcodegen" >&2
+ exit 3
+fi
+
+# Read the app's actual bundle identifier from its Info.plist so we don't
+# guess wrong if the CN1 generator changes its default.
+BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_BUNDLE/Info.plist" 2>/dev/null || true)"
+if [ -z "$BUNDLE_ID" ]; then
+ iv_log "Could not read CFBundleIdentifier from $APP_BUNDLE/Info.plist" >&2
+ exit 3
+fi
+iv_log "Bundle id: $BUNDLE_ID"
+
+DEVICE_NAME="${CN1IV_DEVICE_NAME:-}"
+DEVICE_RUNTIME="${CN1IV_DEVICE_RUNTIME:-}"
+
+# Build a sorted (name, runtime, udid) list of available simulators. Newer
+# iOS runtimes sort last so we pick them by default.
+read_devices() {
+ xcrun simctl list devices available -j \
+ | python3 -c '
+import json, sys
+data = json.load(sys.stdin)
+rows = []
+for runtime, devs in data.get("devices", {}).items():
+ if "iOS-" not in runtime:
+ continue
+ for d in devs:
+ if d.get("isAvailable"):
+ rows.append((runtime, d["name"], d["udid"]))
+rows.sort()
+for r in rows:
+ print("\t".join(r))
+'
+}
+
+SIM_UDID=""
+if [ -n "$DEVICE_NAME" ]; then
+ iv_log "Locating simulator by name: $DEVICE_NAME"
+ while IFS=$'\t' read -r runtime name udid; do
+ if [ "$name" = "$DEVICE_NAME" ] && { [ -z "$DEVICE_RUNTIME" ] || [ "$runtime" = "$DEVICE_RUNTIME" ]; }; then
+ SIM_UDID="$udid"
+ break
+ fi
+ done < <(read_devices)
+fi
+
+if [ -z "$SIM_UDID" ]; then
+ # Fall back to the newest available iPhone (any model). XCode 16.4 only has
+ # iPhone 16; XCode 26 has iPhone 17. We'd rather adapt than fail-fast on a
+ # CI runner that doesn't have the exact device name we'd prefer.
+ iv_log "No exact device match -- picking the newest available iPhone"
+ while IFS=$'\t' read -r runtime name udid; do
+ case "$name" in
+ iPhone*) SIM_UDID="$udid"; DEVICE_NAME="$name"; DEVICE_RUNTIME="$runtime" ;;
+ esac
+ done < <(read_devices)
+fi
+
+if [ -z "$SIM_UDID" ]; then
+ iv_log "No iOS simulator available on this host" >&2
+ xcrun simctl list devices available >&2 || true
+ exit 3
+fi
+iv_log "Selected simulator: $DEVICE_NAME ($DEVICE_RUNTIME)"
+iv_log "Using simulator $SIM_UDID"
+
+# Boot the simulator if needed. `bootstatus -b` blocks until SpringBoard is up.
+xcrun simctl boot "$SIM_UDID" >/dev/null 2>&1 || true
+xcrun simctl bootstatus "$SIM_UDID" -b
+
+# Install the app fresh -- uninstall first so a stale bundle doesn't shadow the
+# new one when the bundle identifier collides.
+xcrun simctl uninstall "$SIM_UDID" "$BUNDLE_ID" >/dev/null 2>&1 || true
+iv_log "Installing $APP_BUNDLE"
+xcrun simctl install "$SIM_UDID" "$APP_BUNDLE"
+
+# Start streaming os_log lines that came from the CN1 process (printf -> NSLog
+# on the iOS port routes through unified logging). Capture in the background;
+# we'll wait on the file after the XCUITest run.
+iv_log "Starting log stream -> $LOG_FILE"
+: > "$LOG_FILE"
+xcrun simctl spawn "$SIM_UDID" log stream \
+ --style compact --level debug \
+ --predicate '(processImagePath CONTAINS[c] "'"$BUNDLE_ID"'") OR (eventMessage CONTAINS "CN1IV:")' \
+ > "$LOG_FILE" 2>&1 &
+LOG_PID=$!
+cleanup() {
+ kill "$LOG_PID" 2>/dev/null || true
+ wait "$LOG_PID" 2>/dev/null || true
+}
+trap cleanup EXIT INT TERM
+
+# Generate the XCUITest Xcode project on demand. We don't check in pbxproj.
+iv_log "Generating XCUITest project via xcodegen"
+( cd "$TESTS_DIR" && xcodegen generate >> "$XCODEBUILD_LOG" 2>&1 )
+
+# Run the XCUITest suite. The Swift code uses XCUIApplication(bundleIdentifier:)
+# with a hard-coded id that mirrors common/codenameone_settings.properties --
+# xcodebuild `KEY=VALUE` args are build settings, not runtime env vars, so we
+# can't pass the bundle id through here. The driver verifies the installed
+# bundle id matches what the test will request before launching xcodebuild,
+# rather than trying to thread it into the test process.
+EXPECTED_BUNDLE_ID="com.codenameone.inputvalidation"
+if [ "$BUNDLE_ID" != "$EXPECTED_BUNDLE_ID" ]; then
+ iv_log "WARNING: installed bundle id ($BUNDLE_ID) does not match the value"
+ iv_log "WARNING: hard-coded in InputValidationUITests.swift ($EXPECTED_BUNDLE_ID)."
+ iv_log "WARNING: Update both or XCUITest will fail to launch the app."
+fi
+# `-resultBundlePath` captures the .xcresult so we can extract the actual
+# test failure reason post-hoc (without it, xcodebuild just prints
+# `** TEST FAILED **`).
+XCRESULT_BUNDLE="$ARTIFACTS_DIR/test.xcresult"
+rm -rf "$XCRESULT_BUNDLE"
+iv_log "Running XCUITest"
+set +e
+xcodebuild test \
+ -project "$TESTS_DIR/CN1InputValidationUITests.xcodeproj" \
+ -scheme CN1InputValidationUITests \
+ -destination "platform=iOS Simulator,id=$SIM_UDID" \
+ -resultBundlePath "$XCRESULT_BUNDLE" \
+ CODE_SIGNING_ALLOWED=NO \
+ | tee -a "$XCODEBUILD_LOG"
+XCB_RC=${PIPESTATUS[0]}
+set -e
+iv_log "xcodebuild test exit=$XCB_RC"
+
+# Extract the human-readable failure summary if the result bundle is present
+# so the artifact upload has something searchable beyond the opaque
+# "** TEST FAILED **" line in xcodebuild-test.log.
+if [ -d "$XCRESULT_BUNDLE" ]; then
+ iv_log "Extracting xcresult diagnostics"
+ xcrun xcresulttool get test-results summary --path "$XCRESULT_BUNDLE" --format json \
+ > "$ARTIFACTS_DIR/xcresult-summary.json" 2>/dev/null || true
+ xcrun xcresulttool get log --type action --path "$XCRESULT_BUNDLE" \
+ > "$ARTIFACTS_DIR/xcresult-action.log" 2>/dev/null || true
+fi
+
+# Give the log stream a beat to flush the final CN1IV:SUITE:FINISHED line.
+sleep 2
+cleanup
+trap - EXIT INT TERM
+
+# Assertion: each expected event must appear at least once in the log.
+REQUIRED_EVENTS=(
+ "CN1IV:READY:tap"
+ "CN1IV:EVENT:tap"
+ "CN1IV:READY:drag"
+ "CN1IV:EVENT:drag"
+ "CN1IV:READY:longpress"
+ "CN1IV:EVENT:longpress"
+ "CN1IV:SUITE:FINISHED"
+)
+FAILED=0
+for needle in "${REQUIRED_EVENTS[@]}"; do
+ if grep -q "$needle" "$LOG_FILE"; then
+ iv_log "OK $needle"
+ else
+ iv_log "MISS $needle"
+ FAILED=1
+ fi
+done
+
+if grep -qE 'CN1IV:TIMEOUT:' "$LOG_FILE"; then
+ iv_log "Gesture timeouts detected in device log:"
+ grep -E 'CN1IV:TIMEOUT:' "$LOG_FILE" | sed 's/^/ /'
+ FAILED=1
+fi
+
+if [ "$XCB_RC" -ne 0 ]; then
+ iv_log "xcodebuild test failed (rc=$XCB_RC) -- see $XCODEBUILD_LOG"
+ FAILED=1
+fi
+
+if [ "$FAILED" -ne 0 ]; then
+ iv_log "Input-validation suite FAILED -- see $LOG_FILE"
+ exit 1
+fi
+
+iv_log "Input-validation suite PASSED"
diff --git a/scripts/input-validation-app/ios-tests/HostStub/HostStubApp.swift b/scripts/input-validation-app/ios-tests/HostStub/HostStubApp.swift
new file mode 100644
index 0000000000..85bb62cbfd
--- /dev/null
+++ b/scripts/input-validation-app/ios-tests/HostStub/HostStubApp.swift
@@ -0,0 +1,26 @@
+// Minimal UIKit host app for the XCUITest target. Application-only UI
+// testing (no host app, USES_XCTRUNNER) errored with an opaque
+// `** TEST FAILED **` under Xcode 16.4. Giving the test bundle a regular
+// host -- even one that does nothing -- is the standard XCUITest setup and
+// removes that failure mode. The actual UI tests attach to the
+// already-installed CN1 input-validation app by bundle id via
+// XCUIApplication(bundleIdentifier:), so what this stub does is irrelevant
+// -- it just needs to be a launchable app the test runner can host into.
+
+import UIKit
+
+@UIApplicationMain
+final class HostStubAppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+
+ func application(_ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
+ let w = UIWindow(frame: UIScreen.main.bounds)
+ let vc = UIViewController()
+ vc.view.backgroundColor = .black
+ w.rootViewController = vc
+ w.makeKeyAndVisible()
+ self.window = w
+ return true
+ }
+}
diff --git a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift
new file mode 100644
index 0000000000..725871a2a4
--- /dev/null
+++ b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift
@@ -0,0 +1,74 @@
+// Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+// DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+//
+// XCUITest target that drives the CN1 input-validation app through tap,
+// drag, and long-press gestures on the iOS simulator. We rely on coordinate
+// taps rather than accessibility queries because the CN1 iOS port does not
+// surface child Components as XCUIElements -- the whole CN1 form renders into
+// one GL/Metal-backed view from XCUITest's perspective. The driver shell
+// script asserts the CN1IV:EVENT:* lines appear in the os_log stream; this
+// file only sequences the physical inputs.
+
+import XCTest
+
+final class InputValidationUITests: XCTestCase {
+ // Bundle identifier of the CN1-built iOS app under test. The CN1 maven
+ // plugin derives the iOS CFBundleIdentifier from
+ // `codename1.packageName` in common/codenameone_settings.properties, so
+ // keeping that property and this default in sync is enough. The
+ // CN1IV_BUNDLE_ID env var override is for local runs against an app
+ // built with a different packageName.
+ private var bundleIdentifier: String {
+ return ProcessInfo.processInfo.environment["CN1IV_BUNDLE_ID"]
+ ?? "com.codenameone.inputvalidation"
+ }
+
+ private var stepDelaySeconds: TimeInterval {
+ if let raw = ProcessInfo.processInfo.environment["CN1IV_STEP_DELAY_SEC"],
+ let v = Double(raw) {
+ return v
+ }
+ return 3.0
+ }
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ func testGestureSuite() throws {
+ let app = XCUIApplication(bundleIdentifier: bundleIdentifier)
+ app.launch()
+ // Wait for the form to render before driving inputs. The CN1 EDT needs
+ // a moment after process launch to mount the GLViewController, run the
+ // first paint, and start dispatching pointer events. Without this delay
+ // taps fire into the splash screen.
+ Thread.sleep(forTimeInterval: 2.5)
+
+ try driveTap(app: app)
+ Thread.sleep(forTimeInterval: stepDelaySeconds)
+
+ try driveDrag(app: app)
+ Thread.sleep(forTimeInterval: stepDelaySeconds)
+
+ try driveLongPress(app: app)
+ Thread.sleep(forTimeInterval: stepDelaySeconds)
+ }
+
+ private func driveTap(app: XCUIApplication) throws {
+ let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
+ center.tap()
+ }
+
+ private func driveDrag(app: XCUIApplication) throws {
+ // Sweep horizontally across the middle band so the CN1 drag detector
+ // collects enough pointerDragged samples to exceed its 3-sample floor.
+ let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.55))
+ let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.55))
+ start.press(forDuration: 0.05, thenDragTo: end)
+ }
+
+ private func driveLongPress(app: XCUIApplication) throws {
+ let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
+ center.press(forDuration: 1.5)
+ }
+}
diff --git a/scripts/input-validation-app/ios-tests/project.yml b/scripts/input-validation-app/ios-tests/project.yml
new file mode 100644
index 0000000000..e4bc533fa1
--- /dev/null
+++ b/scripts/input-validation-app/ios-tests/project.yml
@@ -0,0 +1,69 @@
+# XcodeGen project spec for the CN1 input-validation XCUITest target.
+#
+# We don't check in a .xcodeproj -- pbxproj diffs are unreadable and break
+# on every Xcode update. drivers/run-ios.sh runs `xcodegen generate` against
+# this file to materialise the project on demand.
+#
+# Setup:
+# - HostStub: a minimal UIApplication target the XCUITest bundle hosts
+# into. Application-only UI testing (TEST_TARGET_NAME="") errors out
+# opaquely under Xcode 16.4, so we give the test bundle a regular host.
+# The stub does nothing -- the UI tests attach to the already-installed
+# CN1 input-validation app via XCUIApplication(bundleIdentifier:).
+# - CN1InputValidationUITests: the actual UI test bundle. TEST_HOST points
+# at HostStub. Tests run by xcodebuild build the stub + the test bundle,
+# install them, and then the Swift code launches the CN1 app by bundle id.
+
+name: CN1InputValidationUITests
+options:
+ deploymentTarget:
+ iOS: "16.0"
+ minimumXcodeGenVersion: "2.38.0"
+ bundleIdPrefix: com.codenameone.cn1iv
+ createIntermediateGroups: true
+
+settings:
+ base:
+ SWIFT_VERSION: "5.0"
+ CODE_SIGNING_ALLOWED: NO
+ CODE_SIGN_IDENTITY: ""
+
+targets:
+ HostStub:
+ type: application
+ platform: iOS
+ sources:
+ - HostStub
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.codenameone.cn1iv.hoststub
+ INFOPLIST_KEY_UILaunchStoryboardName: ""
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES
+ INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
+ GENERATE_INFOPLIST_FILE: YES
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: YES
+
+ CN1InputValidationUITests:
+ type: bundle.ui-testing
+ platform: iOS
+ sources:
+ - Sources
+ dependencies:
+ - target: HostStub
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.codenameone.cn1iv.uitests
+ TEST_TARGET_NAME: HostStub
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: YES
+
+schemes:
+ CN1InputValidationUITests:
+ build:
+ targets:
+ HostStub: [test]
+ CN1InputValidationUITests: [test]
+ test:
+ targets:
+ - CN1InputValidationUITests
+ gatherCoverageData: false
+ parallelizable: false
diff --git a/scripts/input-validation-app/ios/pom.xml b/scripts/input-validation-app/ios/pom.xml
new file mode 100644
index 0000000000..fa3e8b18fc
--- /dev/null
+++ b/scripts/input-validation-app/ios/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ com.codenameone.examples.inputvalidation
+ cn1-input-validation
+ 1.0-SNAPSHOT
+
+ com.codenameone.examples.inputvalidation
+ cn1-input-validation-ios
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 17
+ 17
+ ios
+ ios
+ ios-device
+
+
+
+
+
+ com.codenameone
+ codenameone-maven-plugin
+ ${cn1.plugin.version}
+
+
+ build-ios
+ package
+
+ build
+
+
+
+
+
+
+
+
+
+ ${project.groupId}
+ ${cn1app.name}-common
+ ${project.version}
+
+
+
diff --git a/scripts/input-validation-app/mvnw b/scripts/input-validation-app/mvnw
new file mode 100755
index 0000000000..b2a8f18832
--- /dev/null
+++ b/scripts/input-validation-app/mvnw
@@ -0,0 +1,327 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+is_java17_home() {
+ if [ -z "$1" ] || [ ! -x "$1/bin/java" ]; then
+ return 1
+ fi
+ "$1/bin/java" -version 2>&1 | head -n 1 | grep -q '"17'
+}
+
+if ! is_java17_home "$JAVA_HOME" ; then
+ for candidate in /usr/lib/jvm/java-17-openjdk-amd64 /usr/lib/jvm/java-17-openjdk /usr/lib/jvm/jdk-17 /usr/lib/jvm/*17*; do
+ if is_java17_home "$candidate" ; then
+ JAVA_HOME="$candidate"
+ export JAVA_HOME
+ break
+ fi
+ done
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/scripts/input-validation-app/pom.xml b/scripts/input-validation-app/pom.xml
new file mode 100644
index 0000000000..b48c037d48
--- /dev/null
+++ b/scripts/input-validation-app/pom.xml
@@ -0,0 +1,90 @@
+
+
+ 4.0.0
+ com.codenameone.examples.inputvalidation
+ cn1-input-validation
+ 1.0-SNAPSHOT
+ pom
+ cn1-input-validation
+
+ Minimal CN1 app whose only job is to validate that input events
+ (tap, drag, long-press) reach Component listeners end-to-end on each
+ port. Driven by per-platform OS-level automation (XCUITest, Playwright).
+
+
+
+ common
+
+
+
+ 8.0-SNAPSHOT
+ 8.0-SNAPSHOT
+ UTF-8
+ 17
+ 3.8.0
+ 17
+ 17
+ cn1-input-validation
+
+
+
+
+
+ com.codenameone
+ java-runtime
+ ${cn1.version}
+
+
+ com.codenameone
+ codenameone-core
+ ${cn1.version}
+
+
+ com.codenameone
+ codenameone-javase
+ ${cn1.version}
+
+
+ com.codenameone
+ codenameone-buildclient
+ ${cn1.version}
+ system
+ ${user.home}/.codenameone/CodeNameOneBuildClient.jar
+
+
+
+
+
+
+
+
+ com.codenameone
+ codenameone-maven-plugin
+ ${cn1.plugin.version}
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+
+
+
+
+
+
+ ios
+
+
+ codename1.platform
+ ios
+
+
+
+ ios
+
+
+
+