diff --git a/apple/testing/default_runner/ios_xctestrun_runner.bzl b/apple/testing/default_runner/ios_xctestrun_runner.bzl
index f866570dc8..62090506a4 100644
--- a/apple/testing/default_runner/ios_xctestrun_runner.bzl
+++ b/apple/testing/default_runner/ios_xctestrun_runner.bzl
@@ -30,6 +30,7 @@ def _get_template_substitutions(
pre_action_binary,
random,
reuse_simulator,
+ screen_capture_format,
sdk_version,
xcodebuild_args,
xctestrun_template,
@@ -47,6 +48,7 @@ def _get_template_substitutions(
"post_action_determines_exit_code": post_action_determines_exit_code,
"pre_action_binary": pre_action_binary,
"reuse_simulator": reuse_simulator,
+ "screen_capture_format": screen_capture_format,
"sdk_version": sdk_version,
# "ordered" isn't a special string, but anything besides "random" for this field runs in order
"test_order": "random" if random else "ordered",
@@ -120,6 +122,7 @@ def _ios_xctestrun_runner_impl(ctx):
pre_action_binary = pre_action_binary,
random = ctx.attr.random,
reuse_simulator = "true" if ctx.attr.reuse_simulator else "false",
+ screen_capture_format = ctx.attr.screen_capture_format,
sdk_version = sdk_version,
xcodebuild_args = " ".join(ctx.attr.xcodebuild_args) if ctx.attr.xcodebuild_args else "",
xctestrun_template = ctx.file._xctestrun_template.short_path,
@@ -248,6 +251,21 @@ dependencies.
default = True,
doc = """
Toggle simulator reuse. The default behavior is to reuse an existing device of the same type and OS version. When disabled, a new simulator is created before testing starts and shutdown when the runner completes.
+""",
+ ),
+ "screen_capture_format": attr.string(
+ default = "",
+ values = ["", "screenshots", "screenRecording"],
+ doc = """
+Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of:
+
+* `""` (default): do not set the key, use Xcode's platform default.
+* `"screenshots"`: capture screenshots only.
+* `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle.
+
+Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot capture the screen) and forces an `.xcresult` bundle to be produced (the capture is only observable there). This attribute is only meaningful for UI tests.
+
+Screen captures are system attachments, so setting this attribute requires `attachment_lifetime` to be `"keepAlways"` or `"deleteOnSuccess"` — with the default `"keepNever"` the capture would be discarded before it reaches the `.xcresult`, so the runner fails with an error.
""",
),
"test_environment": attr.string_dict(
diff --git a/apple/testing/default_runner/ios_xctestrun_runner.template.sh b/apple/testing/default_runner/ios_xctestrun_runner.template.sh
index 5b94efe71b..5bb4f37dbe 100755
--- a/apple/testing/default_runner/ios_xctestrun_runner.template.sh
+++ b/apple/testing/default_runner/ios_xctestrun_runner.template.sh
@@ -21,6 +21,7 @@ custom_xcodebuild_args=(%(xcodebuild_args)s)
device_id=""
command_line_args=(%(command_line_args)s)
attachment_lifetime="%(attachment_lifetime)s"
+screen_capture_format="%(screen_capture_format)s"
destination_timeout="%(destination_timeout)s"
while [[ $# -gt 0 ]]; do
arg="$1"
@@ -38,6 +39,9 @@ while [[ $# -gt 0 ]]; do
--xctestrun_attachment_lifetime=*)
attachment_lifetime="${arg##*=}"
;;
+ --xctestrun_screen_capture_format=*)
+ screen_capture_format="${arg##*=}"
+ ;;
*)
echo "error: Unsupported argument '${arg}'" >&2
exit 1
@@ -538,6 +542,16 @@ if (( ${#custom_xcodebuild_args[@]} )); then
echo "note: Using 'xcodebuild' because '--xcodebuild_args' was provided"
should_use_xcodebuild=true
fi
+if [[ -n "$screen_capture_format" ]]; then
+ echo "note: Using 'xcodebuild' because a screen capture format was requested"
+ should_use_xcodebuild=true
+ # The capture is only observable in the XCResult bundle, so force one.
+ create_xcresult_bundle=true
+ if [[ "$attachment_lifetime" == "keepNever" ]]; then
+ echo "error: 'screen_capture_format' requires 'attachment_lifetime' to be 'keepAlways' or 'deleteOnSuccess'; with 'keepNever' the capture would be discarded before it reaches the .xcresult bundle" >&2
+ exit 1
+ fi
+fi
# Run a pre-action binary, if provided.
pre_action_binary=%(pre_action_binary)s
@@ -556,6 +570,14 @@ if [[ "$should_use_xcodebuild" == true ]]; then
xctestrun_attachment_lifetime_section+=" UserAttachmentLifetime\n"
xctestrun_attachment_lifetime_section+=" $attachment_lifetime"
+ # Set the preferred screen capture format (Xcode 15+). Left empty when the
+ # attribute is unset so we don't override Xcode's platform default.
+ xctestrun_screen_capture_format_section=""
+ if [[ -n "$screen_capture_format" ]]; then
+ xctestrun_screen_capture_format_section+=" PreferredScreenCaptureFormat\n"
+ xctestrun_screen_capture_format_section+=" $screen_capture_format"
+ fi
+
readonly xctestrun_file="$test_tmp_dir/tests.xctestrun"
/usr/bin/sed \
-e "s${sed_delim}BAZEL_INSERT_LIBRARIES${sed_delim}$xctestrun_libraries${sed_delim}g" \
@@ -573,6 +595,7 @@ if [[ "$should_use_xcodebuild" == true ]]; then
-e "s${sed_delim}BAZEL_COVERAGE_OUTPUT_DIR${sed_delim}$test_tmp_dir${sed_delim}g" \
-e "s${sed_delim}BAZEL_COMMAND_LINE_ARGS_SECTION${sed_delim}$xctestrun_cmd_line_args_section${sed_delim}g" \
-e "s${sed_delim}BAZEL_ATTACHMENT_LIFETIME_SECTION${sed_delim}$xctestrun_attachment_lifetime_section${sed_delim}g" \
+ -e "s${sed_delim}BAZEL_SCREEN_CAPTURE_FORMAT_SECTION${sed_delim}$xctestrun_screen_capture_format_section${sed_delim}g" \
-e "s${sed_delim}BAZEL_SKIP_TEST_SECTION${sed_delim}$xctestrun_skip_test_section${sed_delim}g" \
-e "s${sed_delim}BAZEL_ONLY_TEST_SECTION${sed_delim}$xctestrun_only_test_section${sed_delim}g" \
-e "s${sed_delim}BAZEL_ARCHITECTURE${sed_delim}$architecture${sed_delim}g" \
diff --git a/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun b/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun
index 4cca6158df..3b3c7270f3 100644
--- a/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun
+++ b/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun
@@ -39,6 +39,7 @@
BAZEL_TEST_ENVIRONMENT
BAZEL_ATTACHMENT_LIFETIME_SECTION
+BAZEL_SCREEN_CAPTURE_FORMAT_SECTION
ClangProfileDataDirectoryPath
BAZEL_COVERAGE_OUTPUT_DIR
BAZEL_COMMAND_LINE_ARGS_SECTION
diff --git a/doc/rules-ios.md b/doc/rules-ios.md
index 5928e666d0..53dd50192b 100644
--- a/doc/rules-ios.md
+++ b/doc/rules-ios.md
@@ -644,7 +644,8 @@ load("@rules_apple//apple:ios.bzl", "ios_xctestrun_runner")
ios_xctestrun_runner(name, attachment_lifetime, clean_up_simulator_action, command_line_args,
create_simulator_action, create_xcresult_bundle, destination_timeout,
device_type, os_version, post_action, post_action_determines_exit_code,
- pre_action, random, reuse_simulator, test_environment, xcodebuild_args)
+ pre_action, random, reuse_simulator, screen_capture_format, test_environment,
+ xcodebuild_args)
This rule creates a test runner for iOS tests that uses xctestrun files to run
@@ -703,6 +704,7 @@ in Xcode.
| pre_action | A binary to run prior to test execution. Runs after simulator creation. Sets the `$SIMULATOR_UDID` environment variable, in addition to any other variables available to the test runner. | Label | optional | `None` |
| random | Whether to run the tests in random order to identify unintended state dependencies. | Boolean | optional | `False` |
| reuse_simulator | Toggle simulator reuse. The default behavior is to reuse an existing device of the same type and OS version. When disabled, a new simulator is created before testing starts and shutdown when the runner completes. | Boolean | optional | `True` |
+| screen_capture_format | Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of:
* `""` (default): do not set the key, use Xcode's platform default. * `"screenshots"`: capture screenshots only. * `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle.
Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot capture the screen) and forces an `.xcresult` bundle to be produced (the capture is only observable there). This attribute is only meaningful for UI tests.
Screen captures are system attachments, so setting this attribute requires `attachment_lifetime` to be `"keepAlways"` or `"deleteOnSuccess"` — with the default `"keepNever"` the capture would be discarded before it reaches the `.xcresult`, so the runner fails with an error. | String | optional | `""` |
| test_environment | Optional dictionary with the environment variables that are to be propagated into the XCTest invocation. | Dictionary: String -> String | optional | `{}` |
| xcodebuild_args | Arguments to pass to `xcodebuild` when running the test bundle. This means it will always use `xcodebuild test-without-building` to run the test bundle. | List of strings | optional | `[]` |
diff --git a/test/ios_xctestrun_runner_ui_test.sh b/test/ios_xctestrun_runner_ui_test.sh
index 6ba8505ef7..31bfe2ce4e 100644
--- a/test/ios_xctestrun_runner_ui_test.sh
+++ b/test/ios_xctestrun_runner_ui_test.sh
@@ -480,4 +480,53 @@ function test_ios_ui_test_attachment_lifetime_arg() {
expect_log "deleteOnSuccess"
}
+function test_ios_ui_test_default_screen_capture_format_arg() {
+ create_sim_runners
+ create_ios_app
+ create_ios_ui_tests
+ do_ios_test \
+ --test_env=DEBUG_XCTESTRUNNER=1 \
+ --test_filter=PassingUITest/testPass2 \
+ //ios:PassingUITest || fail "should pass"
+
+ # When unset, PreferredScreenCaptureFormat must not be emitted so we don't
+ # override Xcode's platform default.
+ expect_not_log "PreferredScreenCaptureFormat"
+}
+
+function test_ios_ui_test_screen_capture_format_keep_never_fails() {
+ create_sim_runners
+ create_ios_app
+ create_ios_ui_tests
+ # Screen captures are system attachments; with the default
+ # attachment_lifetime of "keepNever" the capture would be silently
+ # discarded, so the runner must fail instead.
+ ! do_ios_test \
+ --test_filter=PassingUITest/testPass2 \
+ --test_arg=--xctestrun_screen_capture_format=screenRecording \
+ //ios:PassingUITest || fail "should fail"
+
+ expect_log "error: 'screen_capture_format' requires 'attachment_lifetime' to be 'keepAlways' or 'deleteOnSuccess'"
+}
+
+function test_ios_ui_test_screen_capture_format_arg() {
+ create_sim_runners
+ create_ios_app
+ create_ios_ui_tests
+ # Screen captures are system attachments, so attachment_lifetime must not
+ # be "keepNever" or the capture is discarded before landing in .xcresult.
+ do_ios_test \
+ --test_env=DEBUG_XCTESTRUNNER=1 \
+ --test_filter=PassingUITest/testPass2 \
+ --test_arg=--xctestrun_screen_capture_format=screenRecording \
+ --test_arg=--xctestrun_attachment_lifetime=keepAlways \
+ //ios:PassingUITest || fail "should pass"
+
+ expect_log "note: Using 'xcodebuild' because a screen capture format was requested"
+ expect_log "PreferredScreenCaptureFormat"
+ expect_log "screenRecording"
+ expect_log "SystemAttachmentLifetime"
+ expect_log "keepAlways"
+}
+
run_suite "ios_ui_test with iOS xctestrun runner bundling tests"