Browse Source
Tests moved to the https://github.com/JetBrains/compose-multiplatform-core repository. ## Release Notes N/Amaster v1.11.0-alpha02+dev3408
41 changed files with 0 additions and 4336 deletions
@ -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" |
||||
``` |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
@ -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" } |
||||
Binary file not shown.
@ -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 |
||||
@ -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" "$@" |
||||
@ -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 |
||||
@ -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 */; |
||||
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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 |
||||
} |
||||
@ -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() |
||||
} |
||||
} |
||||
@ -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)) |
||||
@ -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") |
||||
@ -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")) |
||||
} |
||||
} |
||||
} |
||||
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -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" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -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 |
||||
) |
||||
@ -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) |
||||
} |
||||
} |
||||
} |
||||
@ -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, |
||||
) |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
} |
||||
@ -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() |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 */; |
||||
} |
||||
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<Workspace |
||||
version = "1.0"> |
||||
<FileRef |
||||
location = "self:"> |
||||
</FileRef> |
||||
</Workspace> |
||||
@ -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> |
||||
@ -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; |
||||
@ -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; |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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() |
||||
} |
||||
@ -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 |
||||
@ -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" |
||||
} |
||||
@ -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() |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue