Browse Source

Remove iOS Instrumented tests (#5499)

Tests moved to the
https://github.com/JetBrains/compose-multiplatform-core repository.

## Release Notes
N/A
master v1.11.0-alpha02+dev3408
Andrei Salavei 1 day ago committed by GitHub
parent
commit
13cc6f0e19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 20
      instrumented-test/README.md
  2. 12
      instrumented-test/build.gradle.kts
  3. 11
      instrumented-test/gradle.properties
  4. 14
      instrumented-test/gradle/libs.versions.toml
  5. BIN
      instrumented-test/gradle/wrapper/gradle-wrapper.jar
  6. 7
      instrumented-test/gradle/wrapper/gradle-wrapper.properties
  7. 252
      instrumented-test/gradlew
  8. 94
      instrumented-test/gradlew.bat
  9. 514
      instrumented-test/launcher/Launcher.xcodeproj/project.pbxproj
  10. 7
      instrumented-test/launcher/Launcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  11. 8
      instrumented-test/launcher/Launcher.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  12. 105
      instrumented-test/launcher/Launcher.xcodeproj/xcshareddata/xcschemes/Launcher.xcscheme
  13. 91
      instrumented-test/launcher/Launcher.xcodeproj/xcshareddata/xcschemes/LauncherHost.xcscheme
  14. 28
      instrumented-test/launcher/Launcher.xctestplan
  15. 13
      instrumented-test/launcher/Launcher/Launcher.swift
  16. 24
      instrumented-test/launcher/LauncherHost/main.swift
  17. 37
      instrumented-test/settings.gradle.kts
  18. 117
      instrumented-test/ui-instrumented-test/build.gradle.kts
  19. 650
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/ComponentsAccessibilitySemanticTest.kt
  20. 170
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/LayersAccessibilityTest.kt
  21. 22
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/configuration.kt
  22. 152
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/interaction/BasicInteractionTest.kt
  23. 321
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/AccessibilityTestNode.kt
  24. 60
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/DpRect+Utils.kt
  25. 22
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/OsVersion.kt
  26. 245
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UIKitInstrumentedTest.kt
  27. 45
      instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UITouch+Utils.kt
  28. 311
      instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils.xcodeproj/project.pbxproj
  29. 7
      instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  30. 67
      instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils.xcodeproj/xcshareddata/xcschemes/CMPTestUtils.xcscheme
  31. 10
      instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/HIDEvent.h
  32. 160
      instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/HIDEvent.m
  33. 25
      instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/UITouch+Test.h
  34. 111
      instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/UITouch+Test.m
  35. 13
      instrumented-test/ui-instrumented-test/src/nativeInterop/cinterop/test.def
  36. 73
      instrumented-test/ui-xctest/build.gradle.kts
  37. 6
      instrumented-test/ui-xctest/gradle.properties
  38. 147
      instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/NativeTestObserver.kt
  39. 203
      instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/NativeTestRunner.kt
  40. 148
      instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/configuration.kt
  41. 14
      instrumented-test/ui-xctest/src/nativeInterop/cinterop/XCTest.def

20
instrumented-test/README.md

@ -1,20 +0,0 @@ @@ -1,20 +0,0 @@
# Compose Multiplatform Instrumented Test
## Overview
This project is a Compose Multiplatform module that implements instrumented UI tests Kotlin tests that runs as native XCTest with host app on iOS Simulator.
## Requirements
- Kotlin >= 2.1.0
- Compose Multiplatform 1.8.0-alpha02
- iOS 12+
## Testing
To execute XCTest cases on an iOS Simulator, use:
```shell
cd launcher
xcodebuild test -scheme Launcher -destination "platform=iOS Simulator,name=iPhone 16 Pro"
```

12
instrumented-test/build.gradle.kts

@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}

11
instrumented-test/gradle.properties

@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
#
# Copyright 2025 JetBrains s.r.o. and respective authors and developers.
# Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
#
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx2048M
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8

14
instrumented-test/gradle/libs.versions.toml

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
[versions]
androidx-lifecycle = "2.8.4"
compose-multiplatform = "1.8.0-alpha02"
junit = "4.13.2"
kotlin = "2.1.0"
[libraries]
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

BIN
instrumented-test/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

7
instrumented-test/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
instrumented-test/gradlew vendored

@ -1,252 +0,0 @@ @@ -1,252 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original 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
#
# https://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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
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
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
instrumented-test/gradlew.bat vendored

@ -1,94 +0,0 @@ @@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

514
instrumented-test/launcher/Launcher.xcodeproj/project.pbxproj

@ -1,514 +0,0 @@ @@ -1,514 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
9928B7FF2D32CD78006277AD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9928B7FE2D32CD75006277AD /* main.swift */; };
9928B8042D330AB6006277AD /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9928B8032D330AB6006277AD /* IOKit.framework */; };
9928B82C2D3422C1006277AD /* Launcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9928B82B2D3422C1006277AD /* Launcher.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
997DFD082B18E5DC000B56B5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9975AAC12AEABB5600AF155F /* Project object */;
proxyType = 1;
remoteGlobalIDString = 997DFCF92B18E5D3000B56B5;
remoteInfo = CMPUIKitUtilsTestApp;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
9928B7FE2D32CD75006277AD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
9928B8032D330AB6006277AD /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.1.sdk/System/Library/Frameworks/IOKit.framework; sourceTree = DEVELOPER_DIR; };
9928B82B2D3422C1006277AD /* Launcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Launcher.swift; sourceTree = "<group>"; };
997DFCE32B18D99E000B56B5 /* Launcher.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Launcher.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
997DFCFA2B18E5D3000B56B5 /* LauncherHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LauncherHost.app; sourceTree = BUILT_PRODUCTS_DIR; };
99BE84D22C3467B100E43826 /* Launcher.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Launcher.xctestplan; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
997DFCE02B18D99E000B56B5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9928B8042D330AB6006277AD /* IOKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
997DFCF72B18E5D3000B56B5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9928B8002D330AAA006277AD /* Frameworks */ = {
isa = PBXGroup;
children = (
9928B8032D330AB6006277AD /* IOKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9975AAC02AEABB5600AF155F = {
isa = PBXGroup;
children = (
99BE84D22C3467B100E43826 /* Launcher.xctestplan */,
997DFCE42B18D99E000B56B5 /* Launcher */,
997DFCFB2B18E5D3000B56B5 /* LauncherHost */,
9928B8002D330AAA006277AD /* Frameworks */,
9975AACB2AEABB5600AF155F /* Products */,
);
sourceTree = "<group>";
};
9975AACB2AEABB5600AF155F /* Products */ = {
isa = PBXGroup;
children = (
997DFCE32B18D99E000B56B5 /* Launcher.xctest */,
997DFCFA2B18E5D3000B56B5 /* LauncherHost.app */,
);
name = Products;
sourceTree = "<group>";
};
997DFCE42B18D99E000B56B5 /* Launcher */ = {
isa = PBXGroup;
children = (
9928B82B2D3422C1006277AD /* Launcher.swift */,
);
path = Launcher;
sourceTree = "<group>";
};
997DFCFB2B18E5D3000B56B5 /* LauncherHost */ = {
isa = PBXGroup;
children = (
9928B7FE2D32CD75006277AD /* main.swift */,
);
path = LauncherHost;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
997DFCE22B18D99E000B56B5 /* Launcher */ = {
isa = PBXNativeTarget;
buildConfigurationList = 997DFCEC2B18D99E000B56B5 /* Build configuration list for PBXNativeTarget "Launcher" */;
buildPhases = (
9928B7EB2D326BB1006277AD /* Build Tests */,
997DFCDF2B18D99E000B56B5 /* Sources */,
997DFCE02B18D99E000B56B5 /* Frameworks */,
997DFCE12B18D99E000B56B5 /* Resources */,
);
buildRules = (
);
dependencies = (
997DFD092B18E5DC000B56B5 /* PBXTargetDependency */,
);
name = Launcher;
productName = CMPUIKitUtilsTests;
productReference = 997DFCE32B18D99E000B56B5 /* Launcher.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
997DFCF92B18E5D3000B56B5 /* LauncherHost */ = {
isa = PBXNativeTarget;
buildConfigurationList = 997DFD052B18E5D4000B56B5 /* Build configuration list for PBXNativeTarget "LauncherHost" */;
buildPhases = (
997DFCF62B18E5D3000B56B5 /* Sources */,
997DFCF72B18E5D3000B56B5 /* Frameworks */,
997DFCF82B18E5D3000B56B5 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = LauncherHost;
productName = CMPUIKitUtilsTestApp;
productReference = 997DFCFA2B18E5D3000B56B5 /* LauncherHost.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
9975AAC12AEABB5600AF155F /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1500;
LastUpgradeCheck = 1520;
TargetAttributes = {
997DFCE22B18D99E000B56B5 = {
CreatedOnToolsVersion = 15.0;
LastSwiftMigration = 1610;
TestTargetID = 997DFCF92B18E5D3000B56B5;
};
997DFCF92B18E5D3000B56B5 = {
CreatedOnToolsVersion = 15.0;
LastSwiftMigration = 1610;
};
};
};
buildConfigurationList = 9975AAC42AEABB5600AF155F /* Build configuration list for PBXProject "Launcher" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 9975AAC02AEABB5600AF155F;
productRefGroup = 9975AACB2AEABB5600AF155F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
997DFCE22B18D99E000B56B5 /* Launcher */,
997DFCF92B18E5D3000B56B5 /* LauncherHost */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
997DFCE12B18D99E000B56B5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
997DFCF82B18E5D3000B56B5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
9928B7EB2D326BB1006277AD /* Build Tests */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Build Tests";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :ui-instrumented-test:embedAndSignAppleFrameworkForXcode\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
997DFCDF2B18D99E000B56B5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9928B82C2D3422C1006277AD /* Launcher.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
997DFCF62B18E5D3000B56B5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9928B7FF2D32CD78006277AD /* main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
997DFD092B18E5DC000B56B5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 997DFCF92B18E5D3000B56B5 /* LauncherHost */;
targetProxy = 997DFD082B18E5DC000B56B5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
9975AADA2AEABB5600AF155F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = NO;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
9975AADB2AEABB5600AF155F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = NO;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
997DFCEA2B18D99E000B56B5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = (
"$(inderited)",
"CMPUIKitUtils/**",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-L$SRCROOT/../ui-instrumented-test/build/objc/iphonesimulator.xcarchive/Products/usr/local/lib",
"-lCMPTestUtils",
"-ObjC",
);
PRODUCT_BUNDLE_IDENTIFIER = JetBrains.CMPUIKitUtilsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LauncherHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LauncherHost";
};
name = Debug;
};
997DFCEB2B18D99E000B56B5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = (
"$(inderited)",
"CMPUIKitUtils/**",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-L$SRCROOT/../ui-instrumented-test/build/objc/iphonesimulator.xcarchive/Products/usr/local/lib",
"-lCMPTestUtils",
"-ObjC",
);
PRODUCT_BUNDLE_IDENTIFIER = JetBrains.CMPUIKitUtilsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LauncherHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LauncherHost";
};
name = Release;
};
997DFD062B18E5D4000B56B5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 45226JTYHN;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = JetBrains.CMPUIKitUtilsTestApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
997DFD072B18E5D4000B56B5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 45226JTYHN;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = JetBrains.CMPUIKitUtilsTestApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
9975AAC42AEABB5600AF155F /* Build configuration list for PBXProject "Launcher" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9975AADA2AEABB5600AF155F /* Debug */,
9975AADB2AEABB5600AF155F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
997DFCEC2B18D99E000B56B5 /* Build configuration list for PBXNativeTarget "Launcher" */ = {
isa = XCConfigurationList;
buildConfigurations = (
997DFCEA2B18D99E000B56B5 /* Debug */,
997DFCEB2B18D99E000B56B5 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
997DFD052B18E5D4000B56B5 /* Build configuration list for PBXNativeTarget "LauncherHost" */ = {
isa = XCConfigurationList;
buildConfigurations = (
997DFD062B18E5D4000B56B5 /* Debug */,
997DFD072B18E5D4000B56B5 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 9975AAC12AEABB5600AF155F /* Project object */;
}

7
instrumented-test/launcher/Launcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata generated

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:../Launcher.xcodeproj">
</FileRef>
</Workspace>

8
instrumented-test/launcher/Launcher.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

105
instrumented-test/launcher/Launcher.xcodeproj/xcshareddata/xcschemes/Launcher.xcscheme

@ -1,105 +0,0 @@ @@ -1,105 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1520"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "997DFCF92B18E5D3000B56B5"
BuildableName = "LauncherHost.app"
BlueprintName = "LauncherHost"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Launcher.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "997DFCE22B18D99E000B56B5"
BuildableName = "Launcher.xctest"
BlueprintName = "Launcher"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9928B8082D330C10006277AD"
BuildableName = "adfsdsaf.xctest"
BlueprintName = "adfsdsaf"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "997DFCF92B18E5D3000B56B5"
BuildableName = "LauncherHost.app"
BlueprintName = "LauncherHost"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "997DFCF92B18E5D3000B56B5"
BuildableName = "LauncherHost.app"
BlueprintName = "LauncherHost"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

91
instrumented-test/launcher/Launcher.xcodeproj/xcshareddata/xcschemes/LauncherHost.xcscheme

@ -1,91 +0,0 @@ @@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "997DFCF92B18E5D3000B56B5"
BuildableName = "LauncherHost.app"
BlueprintName = "LauncherHost"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9928B8082D330C10006277AD"
BuildableName = "adfsdsaf.xctest"
BlueprintName = "adfsdsaf"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "997DFCF92B18E5D3000B56B5"
BuildableName = "LauncherHost.app"
BlueprintName = "LauncherHost"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "997DFCF92B18E5D3000B56B5"
BuildableName = "LauncherHost.app"
BlueprintName = "LauncherHost"
ReferencedContainer = "container:Launcher.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

28
instrumented-test/launcher/Launcher.xctestplan

@ -1,28 +0,0 @@ @@ -1,28 +0,0 @@
{
"configurations" : [
{
"id" : "BD8089FB-4512-49DD-9E9F-FE08E8CF5266",
"name" : "Test Scheme Action",
"options" : {
}
}
],
"defaultOptions" : {
"targetForVariableExpansion" : {
"containerPath" : "container:CMPUIKitUtils.xcodeproj",
"identifier" : "997DFCF92B18E5D3000B56B5",
"name" : "CMPUIKitUtilsTestApp"
}
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:Launcher.xcodeproj",
"identifier" : "997DFCE22B18D99E000B56B5",
"name" : "Launcher"
}
}
],
"version" : 1
}

13
instrumented-test/launcher/Launcher/Launcher.swift

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
import XCTest
import InstrumentedTests
class TestLauncher: XCTestCase {
override class var defaultTestSuite: XCTestSuite {
ConfigurationKt.testSuite()
}
}

24
instrumented-test/launcher/LauncherHost/main.swift

@ -1,24 +0,0 @@ @@ -1,24 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print(ProcessInfo.processInfo.environment)
print(ProcessInfo.processInfo.arguments)
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UIViewController()
window?.rootViewController?.view.backgroundColor = .orange
window?.makeKeyAndVisible()
return true
}
}
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))

37
instrumented-test/settings.gradle.kts

@ -1,37 +0,0 @@ @@ -1,37 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
rootProject.name = "instrumented-test"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
google {
mavenContent {
includeGroupAndSubgroups("androidx")
includeGroupAndSubgroups("com.android")
includeGroupAndSubgroups("com.google")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google {
mavenContent {
includeGroupAndSubgroups("androidx")
includeGroupAndSubgroups("com.android")
includeGroupAndSubgroups("com.google")
}
}
mavenCentral()
}
}
include(":ui-instrumented-test")
include(":ui-xctest")

117
instrumented-test/ui-instrumented-test/build.gradle.kts

@ -1,117 +0,0 @@ @@ -1,117 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithSimulatorTests
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
val frameworkName = "CMPTestUtils"
val buildSchemeName = frameworkName
val objcDir = File(project.projectDir, "src/iosMain/objc")
val frameworkSourcesDir = objcDir
val sdkName: String
val destination: String
val architecture: String
if (iosTarget is KotlinNativeTargetWithSimulatorTests) {
sdkName = "iphonesimulator"
destination = "generic/platform=iOS Simulator"
architecture = if (iosTarget.name == "iosSimulatorArm64") "arm64" else "x86_64"
} else {
sdkName = "iphoneos"
destination = "generic/platform=iOS"
architecture = "arm64"
}
val buildDir = project.layout.buildDirectory.dir("objc/${sdkName}.xcarchive").get().asFile.absolutePath
val frameworkPath = File(buildDir,"/Products/usr/local/lib/lib${frameworkName}.a")
val headersPath = File(frameworkSourcesDir, frameworkName)
val compilerArgs = listOf(
"-include-binary", frameworkPath.absolutePath.toString(),
) + "-tr"
iosTarget.compilations.configureEach {
val libTaskName = "${compileTaskProvider.name}ObjCLib"
project.tasks.register(libTaskName, Exec::class.java) {
inputs.dir(frameworkSourcesDir)
.withPropertyName("${frameworkName}-${sdkName}")
.withPathSensitivity(PathSensitivity.RELATIVE)
outputs.cacheIf { true }
outputs.dir(buildDir)
.withPropertyName("${frameworkName}-${sdkName}-archive")
workingDir(frameworkSourcesDir)
commandLine("xcodebuild")
args(
"archive",
"-scheme", buildSchemeName,
"-archivePath", buildDir,
"-sdk", sdkName,
"-destination", destination,
"SKIP_INSTALL=NO",
"BUILD_LIBRARY_FOR_DISTRIBUTION=YES",
"VALID_ARCHS=${architecture}",
"MACH_O_TYPE=staticlib"
)
}
tasks[compileTaskProvider.name].dependsOn(libTaskName)
cinterops.register("test") {
val cinteropTask = tasks[interopProcessingTaskName]
headersPath.listFiles()?.forEach {
if (it.name.endsWith(".h")) {
extraOpts("-header", it.name)
compilerOpts("-I${headersPath}")
}
cinteropTask.inputs.file(it)
}
}
compileTaskProvider.configure {
compilerOptions {
freeCompilerArgs.addAll(compilerArgs)
}
}
}
iosTarget.binaries {
framework {
baseName = "InstrumentedTests"
isStatic = true
linkerOpts(
"-ObjC",
"-framework", "UIKit",
"-framework", "IOKit"
)
}
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
api(project(":ui-xctest"))
}
}
}

650
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/ComponentsAccessibilitySemanticTest.kt

@ -1,650 +0,0 @@ @@ -1,650 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.ui.accessibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.test.utils.assertAccessibilityTree
import androidx.compose.test.utils.available
import androidx.compose.test.utils.findNodeWithTag
import androidx.compose.test.utils.runUIKitInstrumentedTest
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.viewinterop.UIKitInteropProperties
import androidx.compose.ui.viewinterop.UIKitView
import platform.UIKit.*
import kotlin.test.*
class ComponentsAccessibilitySemanticTest {
@OptIn(ExperimentalMaterialApi::class)
@Test
fun testProgressNodesSemantic() = runUIKitInstrumentedTest {
var sliderValue = 0.4f
setContentWithAccessibilityEnabled {
Column {
Slider(
value = sliderValue,
onValueChange = { sliderValue = it }
)
LinearProgressIndicator(progress = 0.7f)
RangeSlider(
value = 30f..70f,
onValueChange = {},
valueRange = 0f..100f
)
}
}
assertAccessibilityTree {
// Slider
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitAdjustable)
value = "40%"
}
// LinearProgressIndicator
node {
isAccessibilityElement = true
value = "70%"
traits()
}
// Range Slider
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitAdjustable)
value = "43%"
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitAdjustable)
value = "57%"
}
}
}
@Test
fun testSliderAction() = runUIKitInstrumentedTest {
var sliderValue = 0.4f
setContentWithAccessibilityEnabled {
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
modifier = Modifier.testTag("Slider")
)
}
var oldValue = sliderValue
val sliderNode = findNodeWithTag("Slider")
sliderNode.element?.accessibilityIncrement()
assertTrue(oldValue < sliderValue)
oldValue = sliderValue
sliderNode.element?.accessibilityDecrement()
assertTrue(oldValue > sliderValue)
}
@Test
fun testToggleAndCheckboxSemantic() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column {
Switch(false, {})
Checkbox(false, {})
TriStateCheckbox(ToggleableState.On, {})
TriStateCheckbox(ToggleableState.Off, {})
TriStateCheckbox(ToggleableState.Indeterminate, {})
}
}
assertAccessibilityTree {
// Switch
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
if (available(iosMajorVersion = 17)) {
traits(UIAccessibilityTraitToggleButton)
}
}
// Checkbox
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
// ToggleableState
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitSelected
)
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
}
}
@Test
fun testToggleAndCheckboxAction() = runUIKitInstrumentedTest {
var switch by mutableStateOf(false)
var checkbox by mutableStateOf(false)
var triStateCheckbox by mutableStateOf(ToggleableState.Off)
setContentWithAccessibilityEnabled {
Column {
Switch(
checked = switch,
onCheckedChange = { switch = it },
modifier = Modifier.testTag("Switch")
)
Checkbox(
checked = checkbox,
onCheckedChange = { checkbox = it },
modifier = Modifier.testTag("Checkbox")
)
TriStateCheckbox(
state = triStateCheckbox,
onClick = { triStateCheckbox = ToggleableState.On },
modifier = Modifier.testTag("TriStateCheckbox")
)
}
}
findNodeWithTag("Switch").element?.accessibilityActivate()
assertTrue(switch)
waitForIdle()
findNodeWithTag("Switch").element?.accessibilityActivate()
assertFalse(switch)
findNodeWithTag("Checkbox").element?.accessibilityActivate()
assertTrue(checkbox)
waitForIdle()
findNodeWithTag("Checkbox").element?.accessibilityActivate()
assertFalse(checkbox)
findNodeWithTag("TriStateCheckbox").element?.accessibilityActivate()
assertEquals(ToggleableState.On, triStateCheckbox)
}
@Test
fun testRadioButtonSelection() = runUIKitInstrumentedTest {
var selectedIndex by mutableStateOf(0)
setContentWithAccessibilityEnabled {
Column {
RadioButton(selected = selectedIndex == 0, onClick = { selectedIndex = 0 })
RadioButton(selected = selectedIndex == 1, onClick = { selectedIndex = 1 })
RadioButton(
selected = selectedIndex == 2,
onClick = { selectedIndex = 2 },
Modifier.testTag("RadioButton")
)
}
}
assertAccessibilityTree {
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitSelected
)
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
}
findNodeWithTag("RadioButton").element?.accessibilityActivate()
assertAccessibilityTree {
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitSelected
)
}
}
selectedIndex = 0
assertAccessibilityTree {
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitSelected
)
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
node {
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
}
}
@Test
fun testImageSemantics() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column {
Image(
ImageBitmap(10, 10),
contentDescription = null,
modifier = Modifier.testTag("Image 1")
)
Image(
ImageBitmap(10, 10),
contentDescription = null,
modifier = Modifier.testTag("Image 2").semantics { role = Role.Image }
)
Image(
ImageBitmap(10, 10),
contentDescription = "Abstract Picture",
modifier = Modifier.testTag("Image 3")
)
}
}
assertAccessibilityTree {
node {
isAccessibilityElement = false
identifier = "Image 1"
traits()
}
node {
isAccessibilityElement = false
identifier = "Image 2"
traits(UIAccessibilityTraitImage)
}
node {
isAccessibilityElement = true
identifier = "Image 3"
label = "Abstract Picture"
traits(UIAccessibilityTraitImage)
}
}
}
@Test
fun testTextSemantics() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column {
Text("Static Text", modifier = Modifier.testTag("Text 1"))
Text("Custom Button", modifier = Modifier.testTag("Text 2").clickable { })
}
}
assertAccessibilityTree {
node {
isAccessibilityElement = true
identifier = "Text 1"
label = "Static Text"
traits(UIAccessibilityTraitStaticText)
}
node {
isAccessibilityElement = true
identifier = "Text 2"
label = "Custom Button"
traits(UIAccessibilityTraitButton)
}
}
}
@Test
fun testDisabledSemantics() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column {
Button({}, enabled = false) {}
TextField("", {}, enabled = false)
Slider(value = 0f, onValueChange = {}, enabled = false)
Switch(checked = false, onCheckedChange = {}, enabled = false)
Checkbox(checked = false, onCheckedChange = {}, enabled = false)
TriStateCheckbox(state = ToggleableState.Off, onClick = {}, enabled = false)
}
}
assertAccessibilityTree {
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitNotEnabled
)
}
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitNotEnabled
)
}
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitAdjustable,
UIAccessibilityTraitNotEnabled
)
}
node {
isAccessibilityElement = true
if (available(iosMajorVersion = 17)) {
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitToggleButton,
UIAccessibilityTraitNotEnabled
)
} else {
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitNotEnabled
)
}
}
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitNotEnabled
)
}
node {
isAccessibilityElement = true
traits(
UIAccessibilityTraitButton,
UIAccessibilityTraitNotEnabled
)
}
}
}
@Test
fun testHeadingSemantics() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Scaffold(topBar = {
TopAppBar {
Text("Header", modifier = Modifier.semantics { heading() })
}
}) {
Column {
Text("Content")
}
}
}
assertAccessibilityTree {
node {
label = "Header"
isAccessibilityElement = true
traits(UIAccessibilityTraitHeader)
}
node {
label = "Content"
isAccessibilityElement = true
traits(UIAccessibilityTraitStaticText)
}
}
}
@Test
fun testSelectionContainer() = runUIKitInstrumentedTest {
@Composable
fun LabeledInfo(label: String, data: String) {
Text(
buildAnnotatedString {
append("$label: ")
append(data)
}
)
}
setContentWithAccessibilityEnabled {
SelectionContainer {
Column {
Text("Title")
LabeledInfo("Subtitle", "subtitle")
LabeledInfo("Details", "details")
}
}
}
assertAccessibilityTree {
node {
label = "Title"
isAccessibilityElement = true
traits(UIAccessibilityTraitStaticText)
}
node {
label = "Subtitle: subtitle"
isAccessibilityElement = true
traits(UIAccessibilityTraitStaticText)
}
node {
label = "Details: details"
isAccessibilityElement = true
traits(UIAccessibilityTraitStaticText)
}
}
}
@Test
fun testVisibleNodes() = runUIKitInstrumentedTest {
var alpha by mutableStateOf(0f)
setContentWithAccessibilityEnabled {
Text("Hidden", modifier = Modifier.graphicsLayer {
this.alpha = alpha
})
}
assertAccessibilityTree {
label = "Hidden"
isAccessibilityElement = false
}
alpha = 1f
assertAccessibilityTree {
label = "Hidden"
isAccessibilityElement = true
}
}
@Test
fun testVisibleNodeContainers() = runUIKitInstrumentedTest {
var alpha by mutableStateOf(0f)
setContentWithAccessibilityEnabled {
Column {
Text("Text 1")
Row(modifier = Modifier.graphicsLayer {
this.alpha = alpha
}) {
Text("Text 2")
Text("Text 3")
}
}
}
assertAccessibilityTree {
node {
label = "Text 1"
isAccessibilityElement = true
}
node {
label = "Text 2"
isAccessibilityElement = false
}
node {
label = "Text 3"
isAccessibilityElement = false
}
}
alpha = 1f
assertAccessibilityTree {
node {
label = "Text 1"
isAccessibilityElement = true
}
node {
label = "Text 2"
isAccessibilityElement = true
}
node {
label = "Text 3"
isAccessibilityElement = true
}
}
}
@Test
fun testAccessibilityContainer() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column(modifier = Modifier.testTag("Container")) {
Text("Text 1")
Text("Text 2")
}
}
assertAccessibilityTree {
identifier = "Container"
isAccessibilityElement = false
node {
label = "Text 1"
isAccessibilityElement = true
}
node {
label = "Text 2"
isAccessibilityElement = true
}
}
}
@ExperimentalComposeUiApi
@Test
fun testAccessibilityInterop() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column(modifier = Modifier.testTag("Container")) {
UIKitView(
factory = {
val view = UIView()
view.setIsAccessibilityElement(true)
view.setAccessibilityLabel("Disabled")
view
},
properties = UIKitInteropProperties(isNativeAccessibilityEnabled = false)
)
UIKitView(
factory = {
val view = UIView()
view.setIsAccessibilityElement(true)
view.setAccessibilityLabel("Enabled")
view
},
properties = UIKitInteropProperties(isNativeAccessibilityEnabled = true)
)
UIKitView(
factory = {
val view = UIView()
view.setIsAccessibilityElement(true)
view.setAccessibilityLabel("Enabled With Tag")
view
},
properties = UIKitInteropProperties(isNativeAccessibilityEnabled = true),
modifier = Modifier.testTag("Container Tag")
)
}
}
assertAccessibilityTree {
identifier = "Container"
isAccessibilityElement = false
node {
label = "Enabled"
isAccessibilityElement = true
}
node {
identifier = "Container Tag"
isAccessibilityElement = false
node {
label = "Enabled With Tag"
isAccessibilityElement = true
}
}
}
}
@Test
fun testChildrenOfCollapsedNode() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column {
Row(modifier = Modifier.testTag("row").clickable {}) {
Text("Foo", modifier = Modifier.testTag("row_title"))
Text("Bar", modifier = Modifier.testTag("row_subtitle"))
}
}
}
assertAccessibilityTree {
node {
label = "Foo\nBar"
identifier = "row"
isAccessibilityElement = true
traits(UIAccessibilityTraitButton)
}
node {
label = "Foo"
identifier = "row_title"
isAccessibilityElement = false
traits(UIAccessibilityTraitStaticText)
}
node {
label = "Bar"
identifier = "row_subtitle"
isAccessibilityElement = false
traits(UIAccessibilityTraitStaticText)
}
}
}
}

170
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/LayersAccessibilityTest.kt

@ -1,170 +0,0 @@ @@ -1,170 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.ui.accessibility
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.test.utils.assertAccessibilityTree
import androidx.compose.test.utils.runUIKitInstrumentedTest
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import kotlin.test.Test
class LayersAccessibilityTest {
@Test
fun testNodesCoveredByPopup() = runUIKitInstrumentedTest {
val topPopup = mutableStateOf(false)
val bottomPopup = mutableStateOf(false)
val topPopupFocusable = mutableStateOf(false)
setContentWithAccessibilityEnabled {
Text("Root")
if (bottomPopup.value) {
Popup {
Text("Popup 1")
}
}
if (topPopup.value) {
Popup(properties = PopupProperties(focusable = topPopupFocusable.value)) {
Text("Popup 2")
}
}
}
assertAccessibilityTree {
label = "Root"
}
bottomPopup.value = true
// Non-focusable popup should not hide content under it for accessibility reader
assertAccessibilityTree {
node {
label = "Root"
}
node {
label = "Popup 1"
}
}
topPopup.value = true
// Non-focusable popup should not hide content under it for accessibility reader
assertAccessibilityTree {
node {
label = "Root"
}
node {
node {
label = "Popup 1"
}
node {
label = "Popup 2"
}
}
}
topPopupFocusable.value = true
// Popup should react on focusable flag change
assertAccessibilityTree {
label = "Popup 2"
}
topPopup.value = false
bottomPopup.value = false
assertAccessibilityTree {
label = "Root"
}
}
@Test
fun testNodesCoveredByDialog() = runUIKitInstrumentedTest {
val showDialog = mutableStateOf(false)
setContentWithAccessibilityEnabled {
Text("Root")
Popup {
Text("Popup")
}
if (showDialog.value) {
Dialog(onDismissRequest = {}) {
Text("Dialog")
}
}
}
assertAccessibilityTree {
node {
label = "Root"
}
node {
label = "Popup"
}
}
showDialog.value = true
// Dialog popup should hide content under it for accessibility reader
assertAccessibilityTree {
label = "Dialog"
}
showDialog.value = false
assertAccessibilityTree {
node {
label = "Root"
}
node {
label = "Popup"
}
}
}
@Test
fun testLayersAppearanceOrder() = runUIKitInstrumentedTest {
val bottomLayer = mutableStateOf(false)
val middleLayers = mutableStateOf(false)
setContentWithAccessibilityEnabled {
Text("Root")
if (bottomLayer.value) {
Popup(properties = PopupProperties(focusable = true)) {
Text("Bottom")
}
}
if (middleLayers.value) {
Popup(properties = PopupProperties(focusable = true)) {
Text("Middle 1")
}
// Non-focusable layer
Popup {
Text("Middle 2")
}
}
Popup(properties = PopupProperties(focusable = true)) {
Text("Top")
}
}
assertAccessibilityTree {
label = "Top"
}
bottomLayer.value = true
// The last added layer should be on top
assertAccessibilityTree {
label = "Bottom"
}
middleLayers.value = true
// The last added layers should be on top
assertAccessibilityTree {
node {
label = "Middle 1"
}
node {
label = "Middle 2"
}
}
}
}

22
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/configuration.kt

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.test
import kotlinx.cinterop.ExperimentalForeignApi
import androidx.compose.xctest.*
import platform.XCTest.XCTestSuite
@Suppress("unused")
@OptIn(ExperimentalForeignApi::class)
fun testSuite(): XCTestSuite = setupXCTestSuite(
// Run all test cases from the tests
// BasicInteractionTest::class,
// LayersAccessibilityTest::class,
// Run test cases from a test
// BasicInteractionTest::testButtonClick,
// LayersAccessibilityTest::testLayersAppearanceOrder
)

152
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/interaction/BasicInteractionTest.kt

@ -1,152 +0,0 @@ @@ -1,152 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.test.interaction
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.test.utils.assertVisibleInContainer
import androidx.compose.test.utils.findNodeWithLabel
import androidx.compose.test.utils.findNodeWithTag
import androidx.compose.test.utils.runUIKitInstrumentedTest
import androidx.compose.test.utils.toDpRect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class BasicInteractionTest {
/**
* Distance in pixels a touch can wander before we think the user is scrolling.
* https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/Constants.uikit.kt#L22
*/
private val CUPERTINO_TOUCH_SLOP = 10.dp
@Test
fun testButtonClick() = runUIKitInstrumentedTest {
var clicks = 0
setContentWithAccessibilityEnabled {
Box(modifier = Modifier.fillMaxSize()) {
Button(
onClick = { clicks++ },
modifier = Modifier
.testTag("Button")
.align(Alignment.Center)
) {
Text("Click me")
}
}
}
assertEquals(0, clicks)
findNodeWithLabel(label = "Click me")
.tap()
assertEquals(1, clicks)
findNodeWithLabel(label = "Click me")
.tap()
assertEquals(2, clicks)
findNodeWithLabel(label = "Click me")
.tap()
assertEquals(3, clicks)
}
@Test
fun testScroll() = runUIKitInstrumentedTest {
val state = ScrollState(0)
var boxRect = DpRect(DpOffset.Zero, DpSize.Zero)
setContentWithAccessibilityEnabled {
Column(modifier = Modifier.fillMaxSize().verticalScroll(state)) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Red)
.testTag("Hidden after scroll box")
)
Box(modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Green)
.testTag("Box")
.onGloballyPositioned { boxRect = it.boundsInWindow().toDpRect(density) }
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(screenSize.height)
.background(Color.White)
)
}
}
touchDown(screenSize.center)
.dragBy(dy = -(100.dp + CUPERTINO_TOUCH_SLOP))
waitForIdle()
assertEquals(100 * density.density, state.value.toFloat())
assertEquals(DpRect(DpOffset.Zero, DpSize(screenSize.width, 100.dp)), boxRect)
}
@Test
fun testDoubleTap() = runUIKitInstrumentedTest {
var doubleClicked = false
setContentWithAccessibilityEnabled {
Column(modifier = Modifier.safeDrawingPadding()) {
Box(modifier = Modifier.size(100.dp).testTag("Clickable").combinedClickable(
onDoubleClick = { doubleClicked = true }
) {})
TextField("Hello Long Text", {}, modifier = Modifier.testTag("TextField"))
}
}
findNodeWithTag("Clickable").doubleTap()
assertTrue(doubleClicked)
}
@Test
fun testTextFieldCallout() = runUIKitInstrumentedTest {
setContentWithAccessibilityEnabled {
Column(modifier = Modifier.safeDrawingPadding()) {
TextField("Hello-long-long-long-long-long-text", {}, modifier = Modifier.testTag("TextField"))
}
}
findNodeWithTag("TextField").doubleTap()
waitForIdle()
// Verify elements from context menu present
findNodeWithLabel("Cut").let {
it.assertVisibleInContainer()
assertTrue(it.isAccessibilityElement ?: false)
}
findNodeWithLabel("Copy").let {
it.assertVisibleInContainer()
assertTrue(it.isAccessibilityElement ?: false)
}
findNodeWithLabel("Paste").let {
it.assertVisibleInContainer()
assertTrue(it.isAccessibilityElement ?: false)
}
findNodeWithLabel("Select All").let {
it.assertVisibleInContainer()
assertTrue(it.isAccessibilityElement ?: false)
}
}
}

321
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/AccessibilityTestNode.kt

@ -1,321 +0,0 @@ @@ -1,321 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.test.utils
import androidx.compose.ui.unit.*
import kotlinx.cinterop.CValue
import kotlin.test.assertEquals
import kotlin.test.fail
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.useContents
import platform.CoreGraphics.CGRect
import platform.UIKit.*
import platform.darwin.NSIntegerMax
import platform.darwin.NSObject
import kotlin.test.assertTrue
/**
* Constructs an accessibility tree representation of the UI hierarchy starting from the window.
*
* This function traverses the accessibility elements and their children to build a structured
* node tree with information about accessibility properties, allowing for analysis and testing
* of the accessibility features of the UI.
*
* @return The root node of the accessibility tree representing the current UI hierarchy,
* or null if the tree cannot be constructed.
*/
@OptIn(ExperimentalForeignApi::class)
internal fun UIKitInstrumentedTest.getAccessibilityTree(): AccessibilityTestNode {
fun buildNode(element: NSObject, level: Int): AccessibilityTestNode {
val children = mutableListOf<AccessibilityTestNode>()
val elements = element.accessibilityElements()
if (elements != null) {
elements.forEach {
children.add(buildNode(it as NSObject, level = level + 1))
}
} else {
val count = element.accessibilityElementCount()
if (count == NSIntegerMax) {
when {
element is UIView -> {
element.subviews.mapNotNull {
children.add(buildNode(it as UIView, level = level + 1))
}
}
}
} else if (count > 0) {
(0 until count).mapNotNull {
val child = element.accessibilityElementAtIndex(it) as NSObject
children.add(buildNode(child, level = level + 1))
}
} else if (element is UIView) {
element.subviews.mapNotNull {
children.add(buildNode(it as UIView, level = level + 1))
}
}
}
return AccessibilityTestNode(
isAccessibilityElement = element.isAccessibilityElement,
identifier = (element as? UIAccessibilityElement)?.accessibilityIdentifier,
label = element.accessibilityLabel,
value = element.accessibilityValue,
frame = element.accessibilityFrame.toDpRect(),
children = children,
traits = allAccessibilityTraits.keys.filter {
element.accessibilityTraits and it != 0.toULong()
},
element = element
).also { node ->
children.forEach { it.parent = node }
}
}
return buildNode(appDelegate.window!!, 0)
}
private val allAccessibilityTraits = mapOf(
UIAccessibilityTraitNone to "UIAccessibilityTraitNone",
UIAccessibilityTraitButton to "UIAccessibilityTraitButton",
UIAccessibilityTraitLink to "UIAccessibilityTraitLink",
UIAccessibilityTraitHeader to "UIAccessibilityTraitHeader",
UIAccessibilityTraitSearchField to "UIAccessibilityTraitSearchField",
UIAccessibilityTraitImage to "UIAccessibilityTraitImage",
UIAccessibilityTraitSelected to "UIAccessibilityTraitSelected",
UIAccessibilityTraitPlaysSound to "UIAccessibilityTraitPlaysSound",
UIAccessibilityTraitKeyboardKey to "UIAccessibilityTraitKeyboardKey",
UIAccessibilityTraitStaticText to "UIAccessibilityTraitStaticText",
UIAccessibilityTraitSummaryElement to "UIAccessibilityTraitSummaryElement",
UIAccessibilityTraitNotEnabled to "UIAccessibilityTraitNotEnabled",
UIAccessibilityTraitUpdatesFrequently to "UIAccessibilityTraitUpdatesFrequently",
UIAccessibilityTraitStartsMediaSession to "UIAccessibilityTraitStartsMediaSession",
UIAccessibilityTraitAdjustable to "UIAccessibilityTraitAdjustable",
UIAccessibilityTraitAllowsDirectInteraction to "UIAccessibilityTraitAllowsDirectInteraction",
UIAccessibilityTraitCausesPageTurn to "UIAccessibilityTraitCausesPageTurn",
UIAccessibilityTraitTabBar to "UIAccessibilityTraitTabBar",
UIAccessibilityTraitToggleButton to "UIAccessibilityTraitToggleButton",
UIAccessibilityTraitSupportsZoom to "UIAccessibilityTraitSupportsZoom"
)
/**
* Represents a node in an accessibility tree, which is used for testing accessibility features
* within a UI hierarchy. This class captures various accessibility properties of UI components
* and structures them into a tree.
*/
internal data class AccessibilityTestNode(
var isAccessibilityElement: Boolean? = null,
var identifier: String? = null,
var label: String? = null,
var value: String? = null,
var frame: DpRect? = null,
var children: List<AccessibilityTestNode>? = null,
var traits: List<UIAccessibilityTraits>? = null,
var element: NSObject? = null,
var parent: AccessibilityTestNode? = null,
) {
fun node(builder: AccessibilityTestNode.() -> Unit) {
children = (children ?: emptyList()) + AccessibilityTestNode().apply(builder)
}
fun traits(vararg trait: UIAccessibilityTraits) {
traits = (traits ?: emptyList()) + trait
}
fun validate(actualNode: AccessibilityTestNode?) {
isAccessibilityElement?.let {
assertEquals(it, actualNode?.isAccessibilityElement)
}
identifier?.let {
assertEquals(it, actualNode?.identifier)
}
label?.let {
assertEquals(it, actualNode?.label)
}
value?.let {
assertEquals(it, actualNode?.value)
}
frame?.let {
assertEquals(it, actualNode?.frame)
}
traits?.let {
assertEquals(it.toSet(), actualNode?.traits?.toSet())
}
children?.let {
assertEquals(it.count(), actualNode?.children?.count())
it.zip(actualNode?.children ?: emptyList()) { validator, child ->
validator.validate(child)
}
}
}
val hasAccessibilityComponents: Boolean = identifier != null ||
isAccessibilityElement == true ||
label != null ||
value != null ||
traits?.isNotEmpty() == true
fun printTree(): String {
val builder = StringBuilder()
fun print(node: AccessibilityTestNode, level: Int) {
val indent = " ".repeat(level)
builder.append(indent)
builder.append(node.label ?: node.identifier ?: "other")
builder.append(" - ${node.frame}")
node.element?.let {
builder.append(" - <${it::class}>")
}
builder.appendLine()
val fieldIndent = "$indent |"
if (node.isAccessibilityElement == true) {
builder.appendLine("$fieldIndent isAccessibilityElement: true")
}
node.identifier?.let {
builder.appendLine("$fieldIndent accessibilityIdentifier: $it")
}
node.label?.let { builder.appendLine("$fieldIndent accessibilityLabel: $it") }
if (node.traits?.isNotEmpty() == true) {
builder.appendLine("$fieldIndent accessibilityTraits:")
node.traits?.forEach {
builder.appendLine("$fieldIndent - ${allAccessibilityTraits.getValue(it)}")
}
}
node.value?.let { builder.appendLine("$fieldIndent accessibilityValue: $it") }
node.element?.accessibilityCustomActions?.takeIf { it.isNotEmpty() }?.let {
builder.appendLine("$fieldIndent accessibilityCustomActions: $it")
}
node.children?.forEach { print(it, level + 1) }
}
print(this, level = 0)
return builder.toString()
}
}
/**
* Normalizes the accessibility nodes tree by analyzing its properties and children.
* Removes all element that are not accessibility elements or does not work as elements containers.
*/
internal fun AccessibilityTestNode.normalized(): AccessibilityTestNode? {
val normalizedChildren = children?.flatMap { child ->
child.normalized()?.let {
if (it.hasAccessibilityComponents || (it.children?.count() ?: 0) > 1) {
listOf(it)
} else {
it.children
}
} ?: emptyList()
} ?: emptyList()
return if (hasAccessibilityComponents || normalizedChildren.count() > 1) {
this.copy(children = normalizedChildren)
} else if (normalizedChildren.count() == 1) {
normalizedChildren.single()
} else {
null
}
}
internal fun AccessibilityTestNode.assertVisibleInContainer() {
var frame = this.frame ?: DpRectZero()
var iterator = parent
while (iterator != null) {
frame = frame.intersect(iterator.frame ?: DpRectZero())
iterator = iterator.parent
}
assertTrue(
frame.width >= 1.dp && frame.height >= 1.dp,
"Element with frame ${this.frame} is not visible or has very small size"
)
}
/**
* Asserts that the current accessibility tree matches the expected structure defined in the
* provided lambda. The expected structure is defined by configuring an `AccessibilityTestNode`,
* which is then validated against the actual normalized accessibility tree. This function waits
* for the UI to be idle before performing the validation.
*
* @param expected A lambda that allows the caller to specify the expected structure and properties
* of the accessibility tree.
*/
internal fun UIKitInstrumentedTest.assertAccessibilityTree(
expected: AccessibilityTestNode.() -> Unit
) {
val validator = AccessibilityTestNode()
with(validator, expected)
assertAccessibilityTree(validator)
}
internal fun UIKitInstrumentedTest.findNodeWithTag(tag: String) = findNodeOrNull {
it.identifier == tag
} ?: fail("Unable to find node with identifier: $tag")
internal fun UIKitInstrumentedTest.findNodeWithLabel(label: String) = findNodeOrNull {
it.label == label
} ?: fail("Unable to find node with label: $label")
internal fun UIKitInstrumentedTest.firstAccessibleNode() =
findNodeOrNull { it.isAccessibilityElement == true }
?: fail("Unable to find accessibility element")
internal fun UIKitInstrumentedTest.findNodeOrNull(
isValid: (AccessibilityTestNode) -> Boolean
): AccessibilityTestNode? {
waitForIdle()
val actualTreeRoot = getAccessibilityTree()
fun check(node: AccessibilityTestNode): AccessibilityTestNode? {
return if (isValid(node)) {
node
} else {
node.children?.firstNotNullOfOrNull(::check)
}
}
return check(node = actualTreeRoot)
}
/**
* Asserts that the current accessibility tree matches the expected structure defined in the
* provided lambda. The expected structure is defined by configuring an `AccessibilityTestNode`,
* which is then validated against the actual normalized accessibility tree. This function waits
* for the UI to be idle before performing the validation.
*
* @param expected The expected accessibility tree structure represented by an instance of
* `AccessibilityTestNode`.
*/
internal fun UIKitInstrumentedTest.assertAccessibilityTree(expected: AccessibilityTestNode) {
waitForIdle()
val actualTreeRoot = getAccessibilityTree()
val normalizedTree = actualTreeRoot.normalized()
try {
expected.validate(normalizedTree)
} catch (e: Throwable) {
val message = "Unable to validate accessibility tree. Expected normalized tree:\n\n" +
"${expected.printTree()}\n" +
"Normalized tree:\n\n${normalizedTree?.printTree()}\n" +
"Actual tree:\n\n${actualTreeRoot.printTree()}\n"
println(message)
throw e
}
}
@OptIn(ExperimentalForeignApi::class)
internal fun CValue<CGRect>.toDpRect() = useContents {
DpRect(
left = origin.x.dp,
top = origin.y.dp,
right = origin.x.dp + size.width.dp,
bottom = origin.y.dp + size.height.dp,
)
}

60
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/DpRect+Utils.kt

@ -1,60 +0,0 @@ @@ -1,60 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.test.utils
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.*
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.useContents
import platform.CoreGraphics.CGPoint
import platform.CoreGraphics.CGPointMake
import platform.UIKit.UIView
@OptIn(ExperimentalForeignApi::class)
internal fun DpOffset.toCGPoint(): CValue<CGPoint> = CGPointMake(x.value.toDouble(), y.value.toDouble())
internal fun DpRect.center(): DpOffset = DpOffset((left + right) / 2, (top + bottom) / 2)
internal fun DpRect.toRect(density: Density): Rect = Rect(
left = left.value * density.density,
right = right.value * density.density,
top = top.value * density.density,
bottom = bottom.value * density.density
)
internal fun Rect.toDpRect(density: Density): DpRect = DpRect(
left = left.dp / density.density,
right = right.dp / density.density,
top = top.dp / density.density,
bottom = bottom.dp / density.density
)
internal fun DpRectZero() = DpRect(0.dp, 0.dp, 0.dp, 0.dp)
internal fun DpRect.intersect(other: DpRect): DpRect {
if (right < other.left || other.right < left) return DpRectZero()
if (bottom < other.top || other.bottom < top) return DpRectZero()
return DpRect(
left = max(left, other.left),
top = max(top, other.top),
right = min(right, other.right),
bottom = min(bottom, other.bottom)
)
}
@OptIn(ExperimentalForeignApi::class)
internal fun CValue<CGPoint>.toDpOffset(): DpOffset = useContents { DpOffset(x.dp, y.dp) }
@OptIn(ExperimentalForeignApi::class)
internal fun UIView.dpRectInWindow() = convertRect(bounds, toView = null).toDpRect()
internal fun<T> List<T>.forEachWithPrevious(block: (T, T) -> Unit) {
var previous: T? = null
for (current in this) {
previous?.let { block(it, current) }
previous = current
}
}

22
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/OsVersion.kt

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.test.utils
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.useContents
import platform.Foundation.NSProcessInfo
@OptIn(ExperimentalForeignApi::class)
internal fun available(iosMajorVersion: Int, iosMinorVersion: Int = 0): Boolean {
return NSProcessInfo.processInfo.operatingSystemVersion.useContents {
when {
majorVersion.toInt() < iosMajorVersion -> false
majorVersion.toInt() > iosMajorVersion -> true
minorVersion.toInt() < iosMinorVersion -> false
else -> true
}
}
}

245
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UIKitInstrumentedTest.kt

@ -1,245 +0,0 @@ @@ -1,245 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.test.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.ui.platform.AccessibilitySyncOptions
import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.ComposeUIViewController
import kotlinx.cinterop.*
import platform.Foundation.*
import platform.UIKit.*
import platform.darwin.NSObject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource
/**
* Sets up the test environment for iOS instrumented tests, runs the given [test][testBlock]
* and then tears down the test environment. Use the methods on [UIKitInstrumentedTest]
* in the test to find compose content and make assertions on it.
* @param [testBlock] The test function.
*/
internal fun runUIKitInstrumentedTest(
testBlock: UIKitInstrumentedTest.() -> Unit
) = with(UIKitInstrumentedTest()) {
try {
testBlock()
} finally {
tearDown()
}
}
/**
* A class designed for instrumented testing of UIKit-related functionality. It provides methods for setting
* content, simulating user interactions, and managing application lifecycle during testing scenarios.
*
* This class is primarily intended for internal use within a Compose multiplatform environment that integrates
* with UIKit APIs on iOS.
*
* Constructor properties are initialized with the attributes of the main screen and a mock delegate to simulate
* the application setup.
*/
@OptIn(ExperimentalForeignApi::class)
internal class UIKitInstrumentedTest {
private val screen = UIScreen.mainScreen()
val density = Density(density = screen.scale.toFloat())
val appDelegate = MockAppDelegate()
val screenSize: DpSize = screen.bounds().useContents { DpSize(size.width.dp, size.height.dp) }
lateinit var hostingViewController: UIViewController
private set
@OptIn(ExperimentalComposeApi::class)
fun setContentWithAccessibilityEnabled(content: @Composable () -> Unit) {
setContent({ accessibilitySyncOptions = AccessibilitySyncOptions.Always }, content)
}
fun setContent(
configure: ComposeUIViewControllerConfiguration.() -> Unit = {},
content: @Composable () -> Unit
) {
// TODO: Use ComposeHostingViewController when moving to compose-multiplatform-core repo
hostingViewController = ComposeUIViewController(
configure = {
enforceStrictPlistSanityCheck = false
configure()
},
content = content
)
appDelegate.setUpWindow(hostingViewController)
waitForIdle()
}
fun tearDown() {
appDelegate.cleanUp()
}
private val isIdle: Boolean
get() {
return false
// TODO: Uncomment. Use proper isIdle implementation when moving to compose-multiplatform-core repo
// val hadSnapshotChanges = Snapshot.current.hasPendingChanges()
// val isApplyObserverNotificationPending = Snapshot.isApplyObserverNotificationPending
//
// val containerInvalidations = hostingViewController.performSelector(NSSelectorFromString("hasInvalidations")) as Boolean
//
// return !hadSnapshotChanges && !isApplyObserverNotificationPending && !containerInvalidations
}
fun waitForIdle(timeoutMillis: Long = 500) {
// TODO: Properly implement `waitForIdle` when moving to compose-multiplatform-core repo
try {
waitUntil(timeoutMillis = timeoutMillis) { isIdle }
} catch (e: Throwable) {
// Do nothing
}
}
fun delay(timeoutMillis: Long) {
val runLoop = NSRunLoop.currentRunLoop()
runLoop.runUntilDate(NSDate.dateWithTimeIntervalSinceNow(timeoutMillis.toDouble() / 1000.0))
}
fun waitUntil(
conditionDescription: String? = null,
timeoutMillis: Long = 5_000,
condition: () -> Boolean
) {
val runLoop = NSRunLoop.currentRunLoop()
val endTime = TimeSource.Monotonic.markNow() + timeoutMillis.milliseconds
while (!condition()) {
if (TimeSource.Monotonic.markNow() > endTime) {
throw AssertionError(conditionDescription ?: "Timeout ${timeoutMillis}ms reached.")
}
runLoop.runUntilDate(NSDate.dateWithTimeIntervalSinceNow(0.005))
}
}
// Touches:
/**
* Simulates a touch-down event at the specified position on the screen.
*
* @param position The position on the root hosting controller.
* @return A UITouch object representing the touch interaction.
*/
fun touchDown(position: DpOffset): UITouch {
val positionOnWindow = hostingViewController.view.convertPoint(
point = position.toCGPoint(),
toView = appDelegate.window()
)
return appDelegate.window()!!.touchDown(positionOnWindow.toDpOffset())
}
/**
* Simulates a tap gesture at the specified position on the screen.
*
* @param position The position on the root hosting controller.
*/
fun tap(position: DpOffset): UITouch {
return touchDown(position).up()
}
/**
* Simulates a tap gesture for a given AccessibilityTestNode.
*/
fun AccessibilityTestNode.tap(): UITouch {
val frame = frame ?: error("Internal error. Frame is missing.")
return tap(frame.center())
}
fun AccessibilityTestNode.doubleTap(): UITouch {
val frame = frame ?: error("Internal error. Frame is missing.")
tap(frame.center())
delay(50)
return tap(frame.center())
}
/**
* Simulates a drag gesture on the screen, moving the touch from its current location to a specified position
* over a given duration.
*
* @param location The target position of the drag in DpOffset.
* @param duration The duration of the drag gesture, defaulting to 0.5 seconds.
* @return The same UITouch instance after completing the drag gesture.
*/
fun UITouch.dragTo(location: DpOffset, duration: Duration = 0.5.seconds): UITouch {
val startLocation = locationInView(appDelegate.window()!!).toDpOffset()
val endLocation = hostingViewController.view.convertPoint(
point = location.toCGPoint(),
toView = appDelegate.window()
).toDpOffset()
val startTime = TimeSource.Monotonic.markNow()
while (TimeSource.Monotonic.markNow() <= startTime + duration) {
val progress = ((TimeSource.Monotonic.markNow() - startTime) / duration).coerceIn(0.0, 1.0)
val touchLocation = lerp(startLocation, endLocation, progress.toFloat())
this.moveToLocationOnWindow(touchLocation)
NSRunLoop.currentRunLoop().runUntilDate(NSDate.dateWithTimeIntervalSinceNow(1.0 / 60))
}
this.moveToLocationOnWindow(endLocation)
return this
}
/**
* Simulates a drag gesture on the screen, moving the touch from its current location by a specified offset
* over a given duration.
*
* @param offset The offset by which the touch is moved, specified as a DpOffset.
* @param duration The duration of the drag gesture, defaulting to 0.5 seconds.
* @return The same UITouch instance after completing the drag gesture.
*/
fun UITouch.dragBy(offset: DpOffset, duration: Duration = 0.5.seconds): UITouch {
return dragTo(location + offset, duration)
}
/**
* Simulates a drag gesture on the screen, moving the touch from its current location by specified x and y offsets
* over a given duration.
*
* @param dx The horizontal offset by which the touch is moved, specified as a Dp. Defaults to 0.dp.
* @param dy The vertical offset by which the touch is moved, specified as a Dp. Defaults to 0.dp.
* @param duration The duration of the drag gesture, specified as a Duration. Defaults to 0.5 seconds.
* @return The same UITouch instance after completing the drag gesture.
*/
fun UITouch.dragBy(dx: Dp = 0.dp, dy: Dp = 0.dp, duration: Duration = 0.5.seconds): UITouch {
return dragBy(DpOffset(dx, dy), duration)
}
val UITouch.location: DpOffset get() {
return locationInView(hostingViewController.view).toDpOffset()
}
}
@OptIn(ExperimentalForeignApi::class)
internal class MockAppDelegate: NSObject(), UIApplicationDelegateProtocol {
private var _window: UIWindow? = null
override fun window(): UIWindow? = _window
fun setUpWindow(viewController: UIViewController) {
UIApplication.sharedApplication().setDelegate(this)
_window = UIWindow(frame = UIScreen.mainScreen.bounds)
_window?.backgroundColor = UIColor.systemBackgroundColor
_window?.rootViewController = viewController
_window?.makeKeyAndVisible()
}
fun cleanUp() {
_window = null
val window = UIWindow(frame = UIScreen.mainScreen.bounds)
window.rootViewController = UIViewController()
window.makeKeyAndVisible()
}
}

45
instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UITouch+Utils.kt

@ -1,45 +0,0 @@ @@ -1,45 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.test.utils
import androidx.compose.ui.unit.DpOffset
import kotlinx.cinterop.ExperimentalForeignApi
import platform.UIKit.UITouch
import platform.UIKit.UITouchPhase
import platform.UIKit.UIWindow
@OptIn(ExperimentalForeignApi::class)
internal fun UIWindow.touchDown(location: DpOffset): UITouch {
return UITouch.touchAtPoint(
point = location.toCGPoint(),
inWindow = this,
tapCount = 1L,
fromEdge = false
).also {
it.send()
}
}
@OptIn(ExperimentalForeignApi::class)
internal fun UITouch.moveToLocationOnWindow(location: DpOffset) {
setLocationInWindow(location.toCGPoint())
setPhase(UITouchPhase.UITouchPhaseMoved)
send()
}
@OptIn(ExperimentalForeignApi::class)
internal fun UITouch.hold(): UITouch {
setPhase(UITouchPhase.UITouchPhaseStationary)
send()
return this
}
@OptIn(ExperimentalForeignApi::class)
internal fun UITouch.up(): UITouch {
setPhase(UITouchPhase.UITouchPhaseEnded)
send()
return this
}

311
instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils.xcodeproj/project.pbxproj

@ -1,311 +0,0 @@ @@ -1,311 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 63;
objects = {
/* Begin PBXBuildFile section */
999869392D479FAB0096554D /* HIDEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 999869362D479FAB0096554D /* HIDEvent.m */; };
9998693A2D479FAB0096554D /* UITouch+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = 999869382D479FAB0096554D /* UITouch+Test.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
995A49382D3023CC0091FB9B /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "include/$(PRODUCT_NAME)";
dstSubfolderSpec = 16;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
995A493A2D3023CC0091FB9B /* libCMPTestUtils.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCMPTestUtils.a; sourceTree = BUILT_PRODUCTS_DIR; };
999869352D479FAB0096554D /* HIDEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HIDEvent.h; sourceTree = "<group>"; };
999869362D479FAB0096554D /* HIDEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HIDEvent.m; sourceTree = "<group>"; };
999869372D479FAB0096554D /* UITouch+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITouch+Test.h"; sourceTree = "<group>"; };
999869382D479FAB0096554D /* UITouch+Test.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITouch+Test.m"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
995A49372D3023CC0091FB9B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
995A49032D301B510091FB9B = {
isa = PBXGroup;
children = (
9998693B2D479FC80096554D /* CMPTestUtils */,
995A490E2D301B510091FB9B /* Products */,
);
sourceTree = "<group>";
wrapsLines = 1;
};
995A490E2D301B510091FB9B /* Products */ = {
isa = PBXGroup;
children = (
995A493A2D3023CC0091FB9B /* libCMPTestUtils.a */,
);
name = Products;
sourceTree = "<group>";
};
9998693B2D479FC80096554D /* CMPTestUtils */ = {
isa = PBXGroup;
children = (
999869352D479FAB0096554D /* HIDEvent.h */,
999869362D479FAB0096554D /* HIDEvent.m */,
999869372D479FAB0096554D /* UITouch+Test.h */,
999869382D479FAB0096554D /* UITouch+Test.m */,
);
path = CMPTestUtils;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
995A49392D3023CC0091FB9B /* CMPTestUtils */ = {
isa = PBXNativeTarget;
buildConfigurationList = 995A49412D3023CC0091FB9B /* Build configuration list for PBXNativeTarget "CMPTestUtils" */;
buildPhases = (
995A49362D3023CC0091FB9B /* Sources */,
995A49372D3023CC0091FB9B /* Frameworks */,
995A49382D3023CC0091FB9B /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = CMPTestUtils;
packageProductDependencies = (
);
productName = CMPTestUtils;
productReference = 995A493A2D3023CC0091FB9B /* libCMPTestUtils.a */;
productType = "com.apple.product-type.library.static";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
995A49042D301B510091FB9B /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastUpgradeCheck = 1610;
TargetAttributes = {
995A49392D3023CC0091FB9B = {
CreatedOnToolsVersion = 16.1;
};
};
};
buildConfigurationList = 995A49072D301B510091FB9B /* Build configuration list for PBXProject "CMPTestUtils" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 995A49032D301B510091FB9B;
minimizedProjectReferenceProxies = 1;
productRefGroup = 995A490E2D301B510091FB9B /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
995A49392D3023CC0091FB9B /* CMPTestUtils */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
995A49362D3023CC0091FB9B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
999869392D479FAB0096554D /* HIDEvent.m in Sources */,
9998693A2D479FAB0096554D /* UITouch+Test.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
995A49182D301B510091FB9B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
995A49192D301B510091FB9B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
995A49422D3023CC0091FB9B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
995A49432D3023CC0091FB9B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
995A49072D301B510091FB9B /* Build configuration list for PBXProject "CMPTestUtils" */ = {
isa = XCConfigurationList;
buildConfigurations = (
995A49182D301B510091FB9B /* Debug */,
995A49192D301B510091FB9B /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
995A49412D3023CC0091FB9B /* Build configuration list for PBXNativeTarget "CMPTestUtils" */ = {
isa = XCConfigurationList;
buildConfigurations = (
995A49422D3023CC0091FB9B /* Debug */,
995A49432D3023CC0091FB9B /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 995A49042D301B510091FB9B /* Project object */;
}

7
instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata generated

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

67
instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils.xcodeproj/xcshareddata/xcschemes/CMPTestUtils.xcscheme

@ -1,67 +0,0 @@ @@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "995A49392D3023CC0091FB9B"
BuildableName = "libCMPTestUtils.a"
BlueprintName = "CMPTestUtils"
ReferencedContainer = "container:CMPTestUtils.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "995A49392D3023CC0091FB9B"
BuildableName = "libCMPTestUtils.a"
BlueprintName = "CMPTestUtils"
ReferencedContainer = "container:CMPTestUtils.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

10
instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/HIDEvent.h

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
#import <UIKit/UIKit.h>
typedef struct __IOHIDEvent * IOHIDEventPtr;
IOHIDEventPtr HIDEventWithTouches(NSArray<UITouch *> *touches) CF_RETURNS_RETAINED;

160
instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/HIDEvent.m

@ -1,160 +0,0 @@ @@ -1,160 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
#import <UIKit/UIKit.h>
#import "HIDEvent.h"
#import <mach/mach_time.h>
typedef enum : uint32_t {
kIOHIDEventTypeNULL,
kIOHIDEventTypeVendorDefined,
kIOHIDEventTypeButton,
kIOHIDEventTypeKeyboard,
kIOHIDEventTypeTranslation,
kIOHIDEventTypeRotation,
kIOHIDEventTypeScroll,
kIOHIDEventTypeScale,
kIOHIDEventTypeZoom,
kIOHIDEventTypeVelocity,
kIOHIDEventTypeOrientation,
kIOHIDEventTypeDigitizer,
} IOHIDEventType;
typedef enum : uint32_t {
kIOHIDDigitizerEventRange = 1<<0,
kIOHIDDigitizerEventTouch = 1<<1,
kIOHIDDigitizerEventPosition = 1<<2,
} IOHIDDigitizerEventMask;
typedef enum : uint32_t {
kIOHIDEventFieldDigitizerX = kIOHIDEventTypeDigitizer << 16,
kIOHIDEventFieldDigitizerY,
kIOHIDEventFieldDigitizerZ,
kIOHIDEventFieldDigitizerButtonMask,
kIOHIDEventFieldDigitizerType,
kIOHIDEventFieldDigitizerIndex,
kIOHIDEventFieldDigitizerIdentity,
kIOHIDEventFieldDigitizerEventMask,
kIOHIDEventFieldDigitizerRange,
kIOHIDEventFieldDigitizerTouch,
kIOHIDEventFieldDigitizerPressure,
kIOHIDEventFieldDigitizerAuxiliaryPressure,
kIOHIDEventFieldDigitizerTwist,
kIOHIDEventFieldDigitizerTiltX,
kIOHIDEventFieldDigitizerTiltY,
kIOHIDEventFieldDigitizerAltitude,
kIOHIDEventFieldDigitizerAzimuth,
kIOHIDEventFieldDigitizerQuality,
kIOHIDEventFieldDigitizerDensity,
kIOHIDEventFieldDigitizerIrregularity,
kIOHIDEventFieldDigitizerMajorRadius,
kIOHIDEventFieldDigitizerMinorRadius,
kIOHIDEventFieldDigitizerCollection,
kIOHIDEventFieldDigitizerCollectionChord,
kIOHIDEventFieldDigitizerChildEventMask,
kIOHIDEventFieldDigitizerIsDisplayIntegrated,
} IOHIDEventFieldDigitizer;
typedef enum : uint32_t {
kIOHIDDigitizerTransducerTypeStylus = 0,
kIOHIDDigitizerTransducerTypePuck,
kIOHIDDigitizerTransducerTypeFinger,
kIOHIDDigitizerTransducerTypeHand
} IOHIDDigitizerTransducerType;
void IOHIDEventAppendEvent(IOHIDEventPtr event, IOHIDEventPtr child);
void IOHIDEventSetIntegerValue(IOHIDEventPtr event, IOHIDEventFieldDigitizer fieldDigitizer, int value);
void IOHIDEventSetSenderID(IOHIDEventPtr event, uint64_t sender);
IOHIDEventPtr IOHIDEventCreateDigitizerEvent(CFAllocatorRef allocator,
AbsoluteTime time,
IOHIDDigitizerTransducerType type,
uint32_t index,
uint32_t identity,
uint32_t eventMask,
uint32_t buttonMask,
double x,
double y,
double z,
double tipPressure,
double barrelPressure,
Boolean range,
Boolean touch,
UInt32 options);
IOHIDEventPtr IOHIDEventCreateDigitizerFingerEventWithQuality(CFAllocatorRef allocator,
AbsoluteTime time,
uint32_t index,
uint32_t identity,
IOHIDDigitizerEventMask eventMask,
double x,
double y,
double z,
double tipPressure,
double twist,
double minorRadius,
double majorRadius,
double quality,
double density,
double irregularity,
Boolean range,
Boolean touch,
UInt32 options);
IOHIDEventPtr HIDEventWithTouches(NSArray<UITouch *> *touches) {
uint64_t absolute_time = mach_absolute_time();
AbsoluteTime time;
time.hi = absolute_time >> 32;
time.lo = (UInt32)absolute_time;
IOHIDEventPtr event = IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault,
time,
kIOHIDDigitizerTransducerTypeHand,
0,
0,
kIOHIDDigitizerEventTouch,
0,
0,
0,
0,
0,
0,
false,
true,
0);
IOHIDEventSetIntegerValue(event, kIOHIDEventFieldDigitizerIsDisplayIntegrated, true);
for (UITouch *touch in touches) {
IOHIDDigitizerEventMask eventMask = (touch.phase == UITouchPhaseMoved) ? kIOHIDDigitizerEventPosition : (kIOHIDDigitizerEventRange | kIOHIDDigitizerEventTouch);
Boolean rangeAndTouch = touch.phase != UITouchPhaseEnded;
CGPoint touchLocation = [touch locationInView:touch.window];
IOHIDEventPtr touchEvent = IOHIDEventCreateDigitizerFingerEventWithQuality(kCFAllocatorDefault,
time,
(uint32_t)[touches indexOfObject:touch] + 1,
2,
eventMask,
touchLocation.x,
touchLocation.y,
0.0,
0,
0,
5.0,
5.0,
1.0,
1.0,
1.0,
rangeAndTouch,
rangeAndTouch,
0);
IOHIDEventSetIntegerValue(touchEvent, kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
IOHIDEventAppendEvent(event, touchEvent);
CFRelease(touchEvent);
}
return event;
}

25
instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/UITouch+Test.h

@ -1,25 +0,0 @@ @@ -1,25 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UITouch (CMPTest)
+ (instancetype)touchAtPoint:(CGPoint)point
inWindow:(UIWindow *)window
tapCount:(NSInteger)tapCount
fromEdge:(BOOL)fromEdge;
@property (assign) UITouchPhase phase;
@property (assign) CGPoint locationInWindow;
- (void)send;
- (void)updateTimestamp;
@end
NS_ASSUME_NONNULL_END

111
instrumented-test/ui-instrumented-test/src/iosMain/objc/CMPTestUtils/UITouch+Test.m

@ -1,111 +0,0 @@ @@ -1,111 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
#import "UITouch+Test.h"
#import <objc/runtime.h>
#import "HIDEvent.h"
@interface UIEvent (CMPTestPrivate)
- (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)arg2;
- (void)_clearTouches;
- (void)_setHIDEvent:(IOHIDEventPtr)event;
@end
@interface UIApplication (CMPTestPrivate)
- (UIEvent *)_touchesEvent;
@end
typedef struct {
unsigned int _firstTouchForView:1;
unsigned int _isTap:1;
unsigned int _isDelayed:1;
unsigned int _sentTouchesEnded:1;
unsigned int _abandonForwardingRecord:1;
} UITouchFlags;
@interface UITouch (CMPTestPrivate)
- (void)setWindow:(UIWindow *)window;
- (void)setView:(UIView *)view;
- (void)setTapCount:(NSInteger)tapCount;
- (void)setIsTap:(BOOL)isTap;
- (void)setTimestamp:(NSTimeInterval)timestamp;
- (void)setGestureView:(UIView *)view;
- (void)_setLocationInWindow:(CGPoint)location resetPrevious:(BOOL)resetPrevious;
- (void)_setIsFirstTouchForView:(BOOL)firstTouchForView;
- (void)_setIsTapToClick:(BOOL)tapToClick;
- (void)_setHidEvent:(IOHIDEventPtr)event;
- (void)_setEdgeType:(NSInteger)edgeType;
- (void)setPhase:(UITouchPhase)touchPhase;
- (UITouchPhase)phase;
@end
@implementation UITouch (CMPTest)
+ (instancetype)touchAtPoint:(CGPoint)point
inWindow:(UIWindow *)window
tapCount:(NSInteger)tapCount
fromEdge:(BOOL)fromEdge {
return [[UITouch alloc] initAtPoint:point inWindow:window tapCount:tapCount fromEdge:fromEdge];
}
- (id)initAtPoint:(CGPoint)point inWindow:(UIWindow *)window tapCount:(NSInteger)tapCount fromEdge:(BOOL)fromEdge {
self = [super init];
if (self) {
UIView *hitTestView = [window hitTest:point withEvent:nil];
[self setWindow:window];
[self setView:hitTestView];
[self setTapCount:tapCount];
[self _setLocationInWindow:point resetPrevious:YES];
[self setPhase:UITouchPhaseBegan];
[self _setEdgeType:fromEdge ? 4 : 0];
[self _setIsFirstTouchForView:YES];
[self updateTimestamp];
if ([self respondsToSelector:@selector(setGestureView:)]) {
[self setGestureView:hitTestView];
}
IOHIDEventPtr event = HIDEventWithTouches(@[self]);
[self _setHidEvent:event];
CFRelease(event);
}
return self;
}
- (void)setLocationInWindow:(CGPoint)locationInWIndow {
[self _setLocationInWindow:locationInWIndow resetPrevious:NO];
}
- (CGPoint)locationInWindow {
return [self locationInView:self.view.window];
}
- (void)updateTimestamp {
[self setTimestamp:[[NSProcessInfo processInfo] systemUptime]];
}
- (void)send {
UIEvent *event = [[UIApplication sharedApplication] _touchesEvent];
IOHIDEventPtr hidEvent = HIDEventWithTouches(@[self]);
[event _setHIDEvent:hidEvent];
[self updateTimestamp];
[event _addTouch:self forDelayedDelivery:NO];
[[UIApplication sharedApplication] sendEvent:event];
}
@end

13
instrumented-test/ui-instrumented-test/src/nativeInterop/cinterop/test.def

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
#
# Copyright 2025 JetBrains s.r.o. and respective authors and developers.
# Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
#
package = androidx.compose.test.utils
language = Objective-C
compilerOpts = -D_Float16=short
headerFilter = UI*
linkerOpts = -framework UIKit -framework IOKit
foreignExceptionMode = objc-wrap

73
instrumented-test/ui-xctest/build.gradle.kts

@ -1,73 +0,0 @@ @@ -1,73 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.konan.target.*
description = "XCTest wrapper of Native kotlin.test"
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
repositories {
mavenCentral()
}
fun frameworksPath(target: KonanTarget): String {
fun getSdkPlatformPath(platform: String) =
ProcessBuilder("xcrun", "--sdk", platform, "--show-sdk-platform-path").execute()
val path = when (target) {
KonanTarget.MACOS_ARM64, KonanTarget.MACOS_X64 -> getSdkPlatformPath("macosx")
KonanTarget.IOS_SIMULATOR_ARM64, KonanTarget.IOS_X64 -> getSdkPlatformPath("iphonesimulator")
KonanTarget.IOS_ARM64 -> getSdkPlatformPath("iphoneos")
else -> error("Target $this is not supported")
}
return "${path}/Developer/Library/Frameworks/"
}
val nativeTargets = mutableListOf<KotlinNativeTarget>()
val hostManager = HostManager()
fun MutableList<KotlinNativeTarget>.addIfEnabledOnHost(target: KotlinNativeTarget) {
if (hostManager.isEnabled(target.konanTarget)) add(target)
}
kotlin {
with(nativeTargets) {
addIfEnabledOnHost(macosX64())
addIfEnabledOnHost(macosArm64())
addIfEnabledOnHost(iosX64())
addIfEnabledOnHost(iosArm64())
addIfEnabledOnHost(iosSimulatorArm64())
forEach {
it.compilations.all {
cinterops {
register("XCTest") {
val path = frameworksPath(it.konanTarget)
compilerOpts("-iframework", path)
}
}
compileTaskProvider.configure {
compilerOptions {
freeCompilerArgs.add("-Xdont-warn-on-error-suppression")
}
}
}
}
}
sourceSets.all {
languageSettings.apply {
optIn("kotlinx.cinterop.BetaInteropApi")
optIn("kotlinx.cinterop.ExperimentalForeignApi")
optIn("kotlin.experimental.ExperimentalNativeApi")
}
}
}
private fun ProcessBuilder.execute(): String {
return start().inputStream.bufferedReader().readLine()
}

6
instrumented-test/ui-xctest/gradle.properties

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
#
# Copyright 2025 JetBrains s.r.o. and respective authors and developers.
# Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
#
kotlin.mpp.enableCInteropCommonization=true

147
instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/NativeTestObserver.kt

@ -1,147 +0,0 @@ @@ -1,147 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package androidx.compose.xctest
import kotlin.native.internal.test.*
import kotlin.time.*
import kotlin.time.Duration
import kotlin.time.DurationUnit
import platform.Foundation.NSError
import platform.darwin.NSObject
import platform.XCTest.*
/**
* Test execution observation.
*
* This is a bridge between XCTest execution and reporting that brings an ability to get results test-by-test.
* It logs tests and notifies listeners set with [testSettings].
* See also [XCTestObservation on Apple documentation](https://developer.apple.com/documentation/xctest/xctestobservation)
*
* @see TestSettings
*/
internal class NativeTestObserver(private val testSettings: TestSettings) : NSObject(), XCTestObservationProtocol {
private val listeners = testSettings.listeners
private val logger = testSettings.logger
private inline fun sendToListeners(event: TestListener.() -> Unit) {
logger.event()
listeners.forEach(event)
}
private fun XCTest.getTestDuration(): Duration =
testRun?.totalDuration
?.toDuration(DurationUnit.SECONDS)
?: Duration.ZERO
/**
* Failed test case execution.
*
* Records test failures sending them to test listeners.
*/
override fun testCase(testCase: XCTestCase, didRecordIssue: XCTIssue) {
if (testCase is XCTestCaseWrapper) {
val duration = testCase.getTestDuration()
val error = didRecordIssue.associatedError as NSError
val throwable = if (error is NSErrorWithKotlinException) {
error.kotlinException
} else {
Throwable(didRecordIssue.compactDescription)
}
sendToListeners { fail(testCase.testCase, throwable, duration.inWholeMilliseconds) }
}
}
/**
* Records expected failures as failed test as soon as such expectations should be processed in the test.
*/
override fun testCase(testCase: XCTestCase, didRecordExpectedFailure: XCTExpectedFailure) {
logger.log("TestCase: $testCase got expected failure: ${didRecordExpectedFailure.failureReason}")
this.testCase(testCase, didRecordExpectedFailure.issue)
}
/**
* Test case finish notification.
* Both successful and failed executions get this notification.
*/
override fun testCaseDidFinish(testCase: XCTestCase) {
val duration = testCase.getTestDuration()
if (testCase.testRun?.hasSucceeded == true) {
if (testCase is XCTestCaseWrapper) {
val test = testCase.testCase
if (!testCase.ignored) {
sendToListeners { pass(test, duration.inWholeMilliseconds) }
}
}
}
}
/**
* Test case start notification.
*/
override fun testCaseWillStart(testCase: XCTestCase) {
if (testCase is XCTestCaseWrapper) {
val test = testCase.testCase
if (testCase.ignored) {
sendToListeners { ignore(test) }
} else {
sendToListeners { start(test) }
}
}
}
/**
* Test suite failure notification.
*
* Logs the failure of the test suite execution.
*/
override fun testSuite(testSuite: XCTestSuite, didRecordIssue: XCTIssue) {
logger.log("TestSuite ${testSuite.name} recorded issue: ${didRecordIssue.compactDescription}")
}
/**
* Test suite expected failure.
*
* Logs the failure of the test suite execution.
* Treat expected failures as ordinary unexpected one.
*/
override fun testSuite(testSuite: XCTestSuite, didRecordExpectedFailure: XCTExpectedFailure) {
logger.log("TestSuite ${testSuite.name} got expected failure: ${didRecordExpectedFailure.failureReason}")
this.testSuite(testSuite, didRecordExpectedFailure.issue)
}
/**
* Test suite finish notification.
*/
override fun testSuiteDidFinish(testSuite: XCTestSuite) {
val duration = testSuite.getTestDuration().inWholeMilliseconds
if (testSuite is XCTestSuiteWrapper) {
sendToListeners { finishSuite(testSuite.testSuite, duration) }
} else if (testSuite.name == TOP_LEVEL_SUITE) {
sendToListeners {
finishIteration(testSettings, 0, duration) // test iterations are not supported
finishTesting(testSettings, duration)
}
}
}
/**
* Test suite start notification.
*/
override fun testSuiteWillStart(testSuite: XCTestSuite) {
if (testSuite is XCTestSuiteWrapper) {
sendToListeners { startSuite(testSuite.testSuite) }
} else if (testSuite.name == TOP_LEVEL_SUITE) {
sendToListeners {
startTesting(testSettings)
startIteration(testSettings, 0, testSettings.testSuites) // test iterations are not supported
}
}
}
override fun debugDescription() = "Native test listener with test settings $testSettings"
}

203
instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/NativeTestRunner.kt

@ -1,203 +0,0 @@ @@ -1,203 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package androidx.compose.xctest
import kotlinx.cinterop.*
import kotlin.native.internal.test.*
import platform.Foundation.*
import platform.Foundation.NSError
import platform.Foundation.NSInvocation
import platform.Foundation.NSString
import platform.Foundation.NSMethodSignature
import platform.UniformTypeIdentifiers.UTTypeSourceCode
import platform.XCTest.*
import platform.objc.*
/**
* An XCTest equivalent of the K/N TestCase.
*
* Wraps the [TestCase] that runs it with a special bridge method created by adding it to a class.
* The idea is to make XCTest invoke them by the created invocation and show the selector as a test name.
* This selector is created as `class.method` that is than naturally represented in XCTest reports including XCode.
*/
internal class XCTestCaseWrapper(val testCase: TestCase) : XCTestCase(dummyInvocation()) {
// Sets XCTest to continue running after failure to match Kotlin Test
override fun continueAfterFailure(): Boolean = true
val ignored = testCase.ignored || testCase.suite.ignored
private val fullTestName = testCase.fullName
init {
// Set custom test name
val newClass = NSClassFromString(testCase.suite.name)
?: objc_allocateClassPair(XCTestCaseWrapper.`class`(), testCase.suite.name, 0UL)!!.also {
objc_registerClassPair(it)
}
object_setClass(this, newClass)
val testName = if (ignored) {
"[ignored] ${testCase.name}"
} else {
testCase.name
}
val selector = NSSelectorFromString(testName)
createRunMethod(selector)
setInvocation(methodSignatureForSelector(selector)?.let { signature ->
@Suppress("CAST_NEVER_SUCCEEDS")
val invocation = NSInvocation.invocationWithMethodSignature(signature as NSMethodSignature)
invocation.setSelector(selector)
invocation.setTarget(this)
invocation
})
}
/**
* Creates and adds method to the metaclass with implementation block
* that gets an XCTestCase instance as self to be run.
*/
private fun createRunMethod(selector: COpaquePointer?) {
val result = class_addMethod(
cls = this.`class`(),
name = selector,
imp = imp_implementationWithBlock(::run),
types = "v@:" // Obj-C type encodings: v (returns void), @ (id self), : (SEL sel)
)
check(result) {
"Internal error: was unable to add method with selector $selector"
}
}
@ObjCAction
private fun run() {
if (ignored) {
// FIXME: to skip the test XCTSkip() should be used.
// But it is not possible to do that due to the KT-43719 and not implemented exception importing.
// For example, _XCTSkipHandler(testName, 0U, "Test $testName is ignored") fails with 'Uncaught Kotlin exception'.
// So, just don't run the test. It will be seen as passed in XCode, but K/N TestListener correctly processes that.
return
}
try {
testCase.doRun()
} catch (throwable: Throwable) {
val stackTrace = throwable.getStackTrace()
val failedStackLine = stackTrace.first {
// try to filter out kotlin.Exceptions and kotlin.test.Assertion inits to poin to the failed stack and line
!it.contains("kfun:kotlin.")
}
// Find path and line number to create source location
val matchResult = Regex("^\\d+ +.* \\((.*):(\\d+):.*\\)$").find(failedStackLine)
val sourceLocation = if (matchResult != null) {
val (file, line) = matchResult.destructured
XCTSourceCodeLocation(file, line.toLong())
} else {
// No debug info to get the path. Still have to record location
XCTSourceCodeLocation(testCase.suite.name, 0L)
}
// Make a stacktrace attachment, encoding it as source code.
// This makes it appear as an attachment in the XCode test results for the failed test.
@Suppress("CAST_NEVER_SUCCEEDS")
val stackAsPayload = (stackTrace.joinToString("\n") as? NSString)?.dataUsingEncoding(NSUTF8StringEncoding)
val stackTraceAttachment = XCTAttachment.attachmentWithUniformTypeIdentifier(
identifier = UTTypeSourceCode.identifier,
name = "Kotlin stacktrace (full)",
payload = stackAsPayload,
userInfo = null
)
val type = when (throwable) {
is AssertionError -> XCTIssueTypeAssertionFailure
else -> XCTIssueTypeUncaughtException
}
// Finally, create and record an issue with all gathered data
val issue = XCTIssue(
type = type,
compactDescription = "$throwable in $fullTestName",
detailedDescription = buildString {
appendLine("Test '$fullTestName' from '${testCase.suite.name}' failed with $throwable")
throwable.cause?.let { appendLine("(caused by ${throwable.cause})") }
},
sourceCodeContext = XCTSourceCodeContext(
callStackAddresses = throwable.getStackTraceAddresses(),
location = sourceLocation
),
// pass the error through the XCTest to the NativeTestObserver
associatedError = NSErrorWithKotlinException(throwable),
attachments = listOf(stackTraceAttachment)
)
testRun?.recordIssue(issue) ?: error("TestRun for the test $fullTestName not found")
}
}
override fun setUp() {
if (!ignored) testCase.doBefore()
}
override fun tearDown() {
if (!ignored) testCase.doAfter()
}
override fun description(): String = buildString {
append(fullTestName)
if (ignored) append("(ignored)")
}
override fun name(): String {
return testCase.name
}
companion object : XCTestCaseMeta() {
/**
* This method is invoked by the XCTest when it discovered XCTestCase instance
* that contains test method.
*
* This method should not be called with the current idea and assumptions.
*/
override fun testCaseWithInvocation(invocation: NSInvocation?): XCTestCase {
error(
"""
This should not happen by default.
Got invocation: ${invocation?.description}
with selector @sel(${NSStringFromSelector(invocation?.selector)})
""".trimIndent()
)
}
private fun dummyInvocation(): NSInvocation {
return NSInvocation.invocationWithMethodSignature(
NSMethodSignature.signatureWithObjCTypes("v@:".cstr.getPointer(Arena())) as NSMethodSignature
)
}
}
}
/**
* This is a NSError-wrapper of Kotlin exception used to pass it through the XCTIssue
* to the XCTestObservation protocol implementation [NativeTestObserver].
* See [NativeTestObserver.testCase] for the usage.
*/
internal class NSErrorWithKotlinException(val kotlinException: Throwable) : NSError(NSCocoaErrorDomain, NSValidationErrorMinimum, null)
/**
* XCTest equivalent of K/N TestSuite.
*/
internal class XCTestSuiteWrapper(val testSuite: TestSuite) : XCTestSuite(testSuite.name) {
private val ignoredSuite: Boolean
get() = testSuite.ignored || testSuite.testCases.all { it.value.ignored }
override fun setUp() {
if (!ignoredSuite) testSuite.doBeforeClass()
}
override fun tearDown() {
if (!ignoredSuite) testSuite.doAfterClass()
}
}

148
instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/configuration.kt

@ -1,148 +0,0 @@ @@ -1,148 +0,0 @@
/*
* Copyright 2025 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package androidx.compose.xctest
import platform.Foundation.*
import platform.XCTest.XCTest
import platform.XCTest.XCTestObservationCenter
import platform.XCTest.XCTestSuite
import kotlin.native.internal.test.*
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KFunction1
// Top level suite name used to hold all Native tests
internal const val TOP_LEVEL_SUITE = "Kotlin/Native test suite"
// Name of the key that contains arguments used to set [TestSettings]
private const val TEST_ARGUMENTS_KEY = "KotlinNativeTestArgs"
/**
* Stores current settings with the filtered test suites, loggers, and listeners.
* Test settings should be initialized by the setup method.
*/
private lateinit var testSettings: TestSettings
@Suppress("unused")
fun setupXCTestSuite(vararg tests: KClass<*>): XCTestSuite =
setupXCTestSuite(tests = tests.toSet(), testCases = null)
@Suppress("unused")
inline fun <reified Class>setupXCTestSuite(vararg tests: KFunction1<Class, *>): XCTestSuite =
setupXCTestSuite(tests = null, testCases = mapOf(Class::class.qualifiedName to tests.toSet()))
/**
* This is an entry-point of XCTestSuites and XCTestCases generation.
* Function returns the XCTest's top level TestSuite that holds all the test cases
* with K/N tests.
* This test suite can be run by either native launcher compiled to bundle or
* by the other test suite (e.g. compiled as a framework).
*/
fun setupXCTestSuite(tests: Set<KClass<*>>? = null, testCases: Map<String?, Set<KFunction<*>>>? = null): XCTestSuite {
val nativeTestSuite = XCTestSuite.testSuiteWithName(TOP_LEVEL_SUITE)
// Initialize settings with the given args
val args = testArguments(TEST_ARGUMENTS_KEY)
testSettings = TestProcessor(GeneratedSuites.suites, args).process()
check(::testSettings.isInitialized) {
"Test settings wasn't set. Check provided arguments and test suites"
}
// Set test observer that will log test execution
XCTestObservationCenter.sharedTestObservationCenter.addTestObserver(
NativeTestObserver(
testSettings
)
)
if (testSettings.runTests) {
val includeAllTests = tests == null && testCases == null
val testSuiteNames = tests?.map { it.qualifiedName }.orEmpty() + testCases?.keys.orEmpty()
fun includeTestSuite(testSuite: TestSuite) =
includeAllTests || testSuiteNames.contains(testSuite.name)
// Generate and add tests to the main suite
testSettings.testSuites.generate(
addTestSuite = { testSuite ->
includeTestSuite(testSuite)
},
addTestCase = { testCase ->
includeTestSuite(testCase.suite) &&
(testCases == null ||
testCases[testCase.suite.name]?.firstOrNull { it.name == testCase.name } != null)
}
).forEach {
nativeTestSuite.addTest(it)
}
if (includeAllTests) {
// Tests created (self-check)
@Suppress("UNCHECKED_CAST")
check(testSettings.testSuites.size == (nativeTestSuite.tests as List<XCTest>).size) {
"The amount of generated XCTest suites should be equal to Kotlin test suites"
}
}
}
return nativeTestSuite
}
/**
* Gets test arguments from the Info.plist using the provided key to create test settings.
*
* @param key a key used in the `Info.plist` file or as environment variable to pass test arguments
*/
@Suppress("UNCHECKED_CAST")
private fun testArguments(key: String): Array<String> {
(NSProcessInfo.processInfo.arguments as? List<String>)?.let {
// Drop the first element containing executable name.
// See https://developer.apple.com/documentation/foundation/nsprocessinfo/1415596-arguments
// Then filter only relevant to the runner arguments.
val args = it.drop(1)
.filter { argument ->
argument.startsWith("--gtest_") || argument.startsWith("--ktest_") ||
argument == "--help" || argument == "-h"
}.toTypedArray()
if (args.isNotEmpty()) return args
}
(NSProcessInfo.processInfo.environment[key] as? String)?.let {
return it.split(" ").toTypedArray()
}
// As we don't know which bundle we are, iterate through all of them
NSBundle.allBundles
.mapNotNull { (it as? NSBundle)?.infoDictionary?.get(key) as? String }
.singleOrNull()
?.let {
return it.split(" ").toTypedArray()
}
return emptyArray()
}
internal val TestCase.fullName get() = "${suite.name}.$name"
private fun Collection<TestSuite>.generate(
addTestSuite: (TestSuite) -> Boolean,
addTestCase: (TestCase) -> Boolean
): List<XCTestSuite> {
return this.filter(addTestSuite).map { suite ->
val xcSuite = XCTestSuiteWrapper(suite)
suite.testCases.values.filter(addTestCase).map { testCase ->
XCTestCaseWrapper(testCase)
}.forEach {
// add test to its test suite wrapper
xcSuite.addTest(it)
}
xcSuite
}
}

14
instrumented-test/ui-xctest/src/nativeInterop/cinterop/XCTest.def

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
#
# Copyright 2025 JetBrains s.r.o. and respective authors and developers.
# Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
#
depends = Foundation darwin posix
language = Objective-C
package = platform.XCTest
modules = XCTest
compilerOpts = -framework XCTest
linkerOpts = -framework XCTest
foreignExceptionMode = objc-wrap
Loading…
Cancel
Save