use std::time::Duration;
use smix_driver::TapMode;
use smix_error::{ExpectationFailure, FailureCode, FailureInit};
use smix_input::{KeyName, SwipeDirection};
use smix_screen::Role;
use crate::{App, focused, id, label, role, role_named, text, text_regex};
use crate::{LaunchFreshOp, plan_launch_fresh_calls};
use crate::{PermissionAction, SimctlPermission};
use super::{Harness, Narration};
#[derive(Debug, Clone)]
pub struct ScenarioEnv {
pub bundle_id: String,
pub app_path: Option<String>,
pub email: Option<String>,
pub org: Option<String>,
pub password: Option<String>,
}
impl ScenarioEnv {
pub fn from_process_env() -> Self {
Self {
bundle_id: std::env::var("SMIX_FIXTURE_BUNDLE_ID")
.unwrap_or_else(|_| "dev.smix.SelftestFixture".to_string()),
app_path: std::env::var("SMIX_FIXTURE_APP_PATH").ok(),
email: std::env::var("SMIX_E2E_EMAIL").ok(),
org: std::env::var("SMIX_E2E_ORG").ok(),
password: std::env::var("SMIX_E2E_PASSWORD").ok(),
}
}
}
pub async fn run_c1_segments(app: &App, harness: &mut Harness, env: &ScenarioEnv) {
let bundle = env.bundle_id.as_str();
seg_plan_launch_fresh_calls(harness).await;
seg_launch_fresh(app, harness, bundle).await;
seg_launch(app, harness, bundle).await;
seg_tree(app, harness).await;
seg_describe(app, harness).await;
seg_screenshot(app, harness, "06-login").await;
seg_selector_text(app, harness).await;
seg_selector_text_regex(app, harness).await;
seg_selector_id(app, harness).await;
seg_selector_label(app, harness).await;
seg_selector_role(app, harness).await;
seg_selector_role_named(app, harness).await;
seg_find(app, harness).await;
seg_find_all(app, harness).await;
seg_find_one(app, harness).await;
seg_system_popups_empty(app, harness).await;
seg_fill(app, harness).await;
seg_focused(app, harness).await;
seg_press_key(app, harness).await;
seg_clear(app, harness).await;
seg_hide_keyboard(app, harness).await;
seg_tap_continue_as_guest(app, harness).await;
seg_tap_with_mode(app, harness).await;
seg_assert_visible(app, harness).await;
seg_assert_enabled(app, harness).await;
seg_assert_text(app, harness).await;
seg_wait_for(app, harness).await;
seg_system_popup_action(app, harness).await;
seg_scroll(app, harness).await;
seg_swipe_once(app, harness).await;
seg_tap_at_coord(app, harness).await;
seg_open_url(app, harness, bundle).await;
seg_foreground(app, harness, bundle).await;
seg_go_back(app, harness).await;
let _ = app.terminate(bundle).await;
tokio::time::sleep(Duration::from_millis(400)).await;
let _ = app.launch(bundle).await;
tokio::time::sleep(Duration::from_millis(1200)).await;
let _ = app.tap(&text("Continue as guest")).await;
tokio::time::sleep(Duration::from_millis(800)).await;
seg_selector_text_regex_with_flags(app, harness).await;
seg_selector_modifiers_spatial(app, harness).await;
seg_selector_modifiers_index(app, harness).await;
seg_expectation_failure_fallback(app, harness).await;
seg_maestro_parity_basic(app, harness).await;
seg_v2_home_basic(app, harness, bundle).await;
seg_v2_form_basic(app, harness, bundle).await;
seg_v2_list_basic(app, harness, bundle).await;
seg_v2_modal_sheet_basic(app, harness, bundle).await;
seg_v2_modal_alert_basic(app, harness, bundle).await;
seg_v2_modal_actionsheet_basic(app, harness, bundle).await;
seg_v2_modal_fullscreen_basic(app, harness, bundle).await;
seg_v2_recording_basic(app, harness, bundle).await;
seg_v2_clipboard_basic(app, harness, bundle).await;
seg_v2_keyboard_basic(app, harness, bundle).await;
seg_v2_orientation_basic(app, harness, bundle).await;
seg_v2_location_basic(app, harness, bundle).await;
seg_v2_permission_basic(app, harness, bundle).await;
seg_v2_argv_basic(app, harness, bundle).await;
seg_v2_deeplink_basic(app, harness, bundle).await;
seg_v2_share_basic(app, harness, bundle).await;
seg_v2_push_basic(app, harness, bundle).await;
seg_v2_picker_basic(app, harness, bundle).await;
seg_v2_auth_basic(app, harness, bundle).await;
seg_v2_anchor_basic(app, harness, bundle).await;
seg_v2_ocr_basic(app, harness, bundle).await;
seg_v2_webview_basic(app, harness, bundle).await;
seg_double_tap_basic(app, harness, bundle).await;
seg_long_press_basic(app, harness, bundle).await;
seg_copy_text_from_basic(app, harness, bundle).await;
seg_set_permissions_batch_basic(app, harness, bundle).await;
seg_install(app, harness).await;
seg_uninstall(app, harness).await;
seg_terminate(app, harness, bundle).await;
}
async fn seg_plan_launch_fresh_calls(harness: &mut Harness) {
let n = Narration::new(
"n/a (pure planner, no sim work)",
"plan_launch_fresh_calls(clear_state=true, clear_keychain=false, app_path=None)",
"Exposes the launch_fresh op sequence as a pure function so the maestro \
adapter can unit-test its warnings + op order without spinning up a sim. \
G10 fallback: clear_state without app_path produces a warning + non-clear \
path (terminate + launch) instead of failing closed.",
"plan_launch_fresh_calls(true, false, None)",
);
let (ops, warnings) = plan_launch_fresh_calls(true, false, None);
let has_terminate = ops
.first()
.is_some_and(|op| matches!(op, LaunchFreshOp::Terminate));
let has_launch = ops
.last()
.is_some_and(|op| matches!(op, LaunchFreshOp::Launch));
if has_terminate && has_launch && !warnings.is_empty() {
harness.record_pass(
"plan_launch_fresh_calls",
n,
&format!(
"{} ops, {} warnings (G10 fallback active)",
ops.len(),
warnings.len()
),
);
} else {
harness.record_fail(
"plan_launch_fresh_calls",
n,
"PlannerInvariantBroken",
&format!(
"expected first=Terminate, last=Launch, warnings>=1; got ops={ops:?} warnings={warnings:?}"
),
None,
);
}
}
async fn seg_launch_fresh(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"(transition — fixture cold-launch)",
"SelftestFixture (clear_state=false → terminate + launch)",
"launch_fresh is the canonical entry point for AI-authored tests: \
maestro `launchApp.clearState` semantics map here. We use the no-wipe \
variant so the run does not depend on app_path being supplied.",
"app.launch_fresh(bundle, false, false, None)",
);
let _ = app.terminate(bundle).await;
tokio::time::sleep(Duration::from_millis(400)).await;
match app.launch_fresh(bundle, false, false, None, &[]).await {
Ok(warnings) => {
tokio::time::sleep(Duration::from_millis(1500)).await;
harness.record_pass(
"launch_fresh",
n,
&format!("launched with {} planner warnings", warnings.len()),
);
}
Err(e) => harness.record_fail("launch_fresh", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_launch(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"(transition — relaunch probe)",
"SelftestFixture (raw launch path, no clear)",
"Plain launch is the lowest-rung lifecycle verb — a thin wrapper over \
`simctl launch`. We probe it idempotently right after launch_fresh: \
re-launching an already-foregrounded app is a no-op that must return \
Ok(()) without resetting UI state.",
"app.launch(bundle)",
);
match app.launch(bundle).await {
Ok(()) => {
tokio::time::sleep(Duration::from_millis(600)).await;
harness.record_pass("launch", n, "idempotent re-launch returned Ok");
}
Err(e) => harness.record_fail("launch", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_tree(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Whole-screen a11y tree (root XCUIElement)",
"tree() is the foundation sense verb — every selector resolves against \
the tree the runner dumps here. AI-authored tests inspect it to choose \
a selector path when find/find_one ambiguity strikes.",
"app.tree()",
);
match app.tree().await {
Ok(node) => {
let count = count_nodes(&node);
if count >= 5 {
harness.record_pass(
"tree",
n,
&format!("{count} nodes; root role={:?}", node.role),
);
} else {
harness.record_fail(
"tree",
n,
"ThinTree",
&format!("tree returned only {count} nodes (expected >= 5 on login screen)"),
None,
);
}
}
Err(e) => harness.record_fail("tree", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_describe(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Whole-screen ScreenDescription (DFS-collected visible+enabled elements)",
"describe() is the AI-readable projection of tree() — DFS visible+enabled \
elements + front_app for failure prompts. Used by every \
ExpectationFailure::visible_elements suggestion path.",
"app.describe()",
);
match app.describe().await {
Ok(desc) => {
harness.record_pass(
"describe",
n,
&format!(
"{} elements; front_app={}",
desc.elements.len(),
desc.front_app
),
);
}
Err(e) => harness.record_fail("describe", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_screenshot(app: &App, harness: &mut Harness, file_stem: &str) {
let n = Narration::new(
"Login form",
"PNG framebuffer dump (via simctl io screenshot)",
"screenshot() is the last-resort sense verb when the a11y tree lies or \
a swizzle break loses Apple defaults (cf. c5i-f). Every selftest run \
lands a login-screen PNG so a human can sanity-check the fixture state \
without re-running the sim.",
"app.screenshot() + fs::write(run_dir/screenshots/<stem>.png)",
);
match app.screenshot().await {
Ok(png) => {
let path = harness
.run_dir()
.join("screenshots")
.join(format!("{file_stem}.png"));
let bytes = png.len();
match std::fs::write(&path, &png) {
Ok(()) => {
if bytes < 50_000 {
harness.record_fail(
"screenshot",
n,
"TruncatedPng",
&format!("PNG smaller than 50KB ({bytes} bytes)"),
Some(path.to_string_lossy().to_string()),
);
} else {
harness.record_pass(
"screenshot",
n,
&format!("{bytes} bytes → {}", path.display()),
);
}
}
Err(e) => {
harness.record_fail(
"screenshot",
n,
"IoError",
&format!("write screenshot: {e}"),
None,
);
}
}
}
Err(e) => harness.record_fail("screenshot", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_selector_text(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Text \"Log in\" (button label)",
"text() is the most common selector factory — mirrors Playwright's \
`getByText`. Resolves via the 6-field OR matcher on label/title/text/value.",
"app.find_one(&text(\"Log in\"))",
);
match app.find_one(&text("Log in")).await {
Ok(Some(_)) => harness.record_pass("text", n, "matched 'Log in' button"),
Ok(None) => {
harness.record_fail("text", n, "NoMatch", "text(\"Log in\") returned None", None)
}
Err(e) => harness.record_fail("text", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_selector_text_regex(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Regex /smix.*selftest/ (matches app-title label)",
"text_regex() = compiled-regex Pattern. Critical for tests that need to \
span dynamic copy (a counter, an env-dependent build tag) without \
growing brittle on exact-match.",
"app.find_one(&text_regex(\"smix.*selftest\"))",
);
match app.find_one(&text_regex("smix.*selftest")).await {
Ok(Some(_)) => harness.record_pass("text_regex", n, "matched smix selftest title"),
Ok(None) => {
harness.record_fail("text_regex", n, "NoMatch", "text_regex returned None", None)
}
Err(e) => harness.record_fail("text_regex", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_selector_id(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"accessibilityIdentifier `input-email`",
"id() is the highest-stability selector — accessibilityIdentifier survives \
text/locale changes. The fixture intentionally annotates every \
interactive surface with a stable id, so id() is the AI's default pick.",
"app.find_one(&id(\"input-email\"))",
);
match app.find_one(&id("input-email")).await {
Ok(Some(_)) => harness.record_pass("id", n, "matched input-email"),
Ok(None) => harness.record_fail("id", n, "NoMatch", "id returned None", None),
Err(e) => harness.record_fail("id", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_selector_label(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Accessibility label \"Continue as guest\"",
"label() targets `node.label` only (not the 6-field OR). When a fixture \
needs to distinguish a button by its VoiceOver label from a visually \
identical sibling, label() is the surgical choice.",
"app.find_one(&label(\"Continue as guest\"))",
);
match app.find_one(&label("Continue as guest")).await {
Ok(Some(_)) => harness.record_pass("label", n, "matched guest button label"),
Ok(None) => harness.record_fail("label", n, "NoMatch", "label returned None", None),
Err(e) => harness.record_fail("label", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_selector_role(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"All XCUIElementTypeButton nodes",
"role(Role::Button) is the broadest selector — used for \"find any \
button\" sweeps. The login screen has 3+ buttons (Log in / Continue as \
guest / Forgot password / coord-target), so a healthy run sees >= 3.",
"app.find_all(&role(Role::Button))",
);
match app.find_all(&role(Role::Button)).await {
Ok(nodes) => {
if nodes.len() >= 3 {
harness.record_pass("role", n, &format!("{} buttons on login", nodes.len()));
} else {
harness.record_fail(
"role",
n,
"TooFewButtons",
&format!("role(Button) returned {} (expected >= 3)", nodes.len()),
None,
);
}
}
Err(e) => harness.record_fail("role", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_selector_role_named(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Button with name == \"Log in\"",
"role_named() composes role() + a text Pattern on the name field. \
Used when role() alone is ambiguous but the AI knows the button's \
human-readable label.",
"app.find_one(&role_named(Role::Button, \"Log in\"))",
);
match app.find_one(&role_named(Role::Button, "Log in")).await {
Ok(Some(_)) => harness.record_pass("role_named", n, "matched named Log in button"),
Ok(None) => {
harness.record_fail("role_named", n, "NoMatch", "role_named returned None", None)
}
Err(e) => harness.record_fail("role_named", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_find(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Boolean presence check for input-email",
"find() is the cheap presence probe — returns bool, never raises on \
not-found. Use it for \"is this screen rendered yet\" predicates \
instead of find_one + .is_some() (clearer intent).",
"app.find(&id(\"input-email\"))",
);
match app.find(&id("input-email")).await {
Ok(true) => harness.record_pass("find", n, "input-email present"),
Ok(false) => harness.record_fail("find", n, "NoMatch", "input-email not present", None),
Err(e) => harness.record_fail("find", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_find_all(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"All XCUIElementTypeTextField nodes",
"find_all() is the multi-match form — same matcher, returns Vec. \
The fixture has at least input-email + input-org + input-password \
text fields on login.",
"app.find_all(&role(Role::TextField))",
);
match app.find_all(&role(Role::TextField)).await {
Ok(nodes) => {
if nodes.len() >= 2 {
harness.record_pass(
"find_all",
n,
&format!("{} text fields on login", nodes.len()),
);
} else {
harness.record_fail(
"find_all",
n,
"TooFewFields",
&format!("got {} TextField nodes (expected >= 2)", nodes.len()),
None,
);
}
}
Err(e) => harness.record_fail("find_all", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_find_one(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"First match for id `input-email`",
"find_one() is the single-match form — returns Option<A11yNode>. \
Distinguished from find() in that the caller can inspect the matched \
node's bounds / role / children for follow-up logic.",
"app.find_one(&id(\"input-email\"))",
);
match app.find_one(&id("input-email")).await {
Ok(Some(node)) => {
harness.record_pass("find_one", n, &format!("got node; role={:?}", node.role))
}
Ok(None) => harness.record_fail("find_one", n, "NoMatch", "find_one returned None", None),
Err(e) => harness.record_fail("find_one", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_system_popups_empty(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"Enumerate system popups (none expected pre-permission)",
"system_popups() is the v4.2 c1 G9 sense capability — enumerates iOS \
system alerts / sheets / banners. Probing it before any permission \
trigger confirms the empty-popup baseline; later (Phase 3) we trigger \
a real camera permission alert.",
"app.system_popups()",
);
match app.system_popups().await {
Ok(popups) => harness.record_pass(
"system_popups",
n,
&format!("capability ok; {} popups visible on login", popups.len()),
),
Err(e) => harness.record_fail("system_popups", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_fill(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form → Email field",
"input-email (UITextField backed by SwiftUI TextField)",
"fill() is smix's primary input verb — every yaml `inputText: \"X\"` \
resolves here. Must drive UIKeyInput chain not host-HID injection \
(sandbox blocked post-iOS-14). smix routes via XCUIElement.typeText.",
"app.fill(&id(\"input-email\"), \"selftest@smix.dev\")",
);
match app.fill(&id("input-email"), "selftest@smix.dev").await {
Ok(()) => harness.record_pass("fill", n, "email field filled with 18 chars"),
Err(e) => harness.record_fail("fill", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_focused(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form (keyboard up after seg_fill)",
"input-email — current first responder via Selector::Focused",
"focused() shortcut emits `Selector::Focused { focused: True(true) }`; \
resolver matches against `A11yNode.has_focus`. v5.1 c1 wired this from \
a KVC walk on `XCElementSnapshot._hasKeyboardFocus` (Apple's public \
dictionaryRepresentation filters the key out, KVC bypasses the filter \
and exposes the live first responder). Pinned after seg_fill so the \
keyboard is up + cursor on input-email is predictable.",
"app.find_one(&focused())",
);
match app.find_one(&focused()).await {
Ok(Some(node)) => {
let got = node.identifier.as_deref().unwrap_or("(no identifier)");
if got == "input-email" {
harness.record_pass(
"focused",
n,
"Selector::Focused resolved to input-email (first responder)",
);
} else {
harness.record_fail(
"focused",
n,
"Mismatch",
&format!(
"focused() returned identifier={got}; expected input-email \
(keyboard up via seg_fill)"
),
None,
);
}
}
Ok(None) => harness.record_fail(
"focused",
n,
"NoMatch",
"Selector::Focused returned None; tree node `has_focus` not set even though \
keyboard should be up after seg_fill",
None,
),
Err(e) => harness.record_fail("focused", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_press_key(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form (keyboard up)",
"Return key — advances focus to next field",
"press_key() dispatches a hardware-key event through the runner's UIKey \
path. KeyName::Return on a TextField typically advances the responder \
chain to the next field; selftest just verifies the key code reached \
the sim without raising.",
"app.press_key(KeyName::Return)",
);
match app.press_key(KeyName::Return).await {
Ok(()) => harness.record_pass("press_key", n, "Return key dispatched"),
Err(e) => harness.record_fail("press_key", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_clear(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form → Email field",
"input-email (clear after fill)",
"clear() is fill()'s inverse — wipes a TextField via select-all + \
delete. Must leave keyboard focus intact (different from \
hide_keyboard).",
"app.clear(&id(\"input-email\"))",
);
match app.clear(&id("input-email")).await {
Ok(()) => harness.record_pass("clear", n, "input-email cleared"),
Err(e) => harness.record_fail("clear", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_hide_keyboard(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form",
"iOS software keyboard (dismiss without tapping outside)",
"hide_keyboard() is the dedicated keyboard-dismiss verb. Avoids the \
common foot-gun of tapping a transparent backdrop and accidentally \
hitting a button beneath; routes via resignFirstResponder.",
"app.hide_keyboard()",
);
match app.hide_keyboard().await {
Ok(()) => harness.record_pass("hide_keyboard", n, "keyboard dismissed"),
Err(e) => harness.record_fail("hide_keyboard", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_tap_continue_as_guest(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Login form → Continue as guest",
"btn-guest (text-selector tap)",
"tap() is the headline act verb — Apple-native-event-chain dispatch. \
This segment transitions the fixture from Login to Home; subsequent \
Phase-3 segments depend on Home being foregrounded.",
"app.tap(&text(\"Continue as guest\"))",
);
match app.tap(&text("Continue as guest")).await {
Ok(()) => {
tokio::time::sleep(Duration::from_millis(1000)).await;
harness.record_pass("tap", n, "tapped guest; transitioning to Home");
}
Err(e) => harness.record_fail("tap", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_tap_with_mode(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard tab",
"tab-dashboard (TabView item)",
"tap_with_mode() exposes the explicit dispatch-mode knob. \
DaemonProxySynthesize bypasses the XCUIElement gesture chain so RN \
Pressable's RCTTouchHandler fires onPress reliably (v4.0 c3 G8 fix); \
we probe the mode parameter on a no-op target (the dashboard title \
text — tapping it changes nothing). Selector must be text-form: the \
runner /tap wire route resolves text-only (id selectors go through \
host-side coord resolution, which has no mode knob).",
"app.tap_with_mode(&text(\"Welcome to smix selftest\"), TapMode::DaemonProxySynthesize)",
);
match app
.tap_with_mode(
&text("Welcome to smix selftest"),
TapMode::DaemonProxySynthesize,
)
.await
{
Ok(()) => {
tokio::time::sleep(Duration::from_millis(500)).await;
harness.record_pass("tap_with_mode", n, "dashboard tab tapped via daemon proxy");
}
Err(e) => harness.record_fail("tap_with_mode", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_assert_visible(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard",
"lbl-dashboard-title (\"Welcome to smix selftest\")",
"assert_visible() = wait_for + NotVisible-coded ExpectationFailure. \
The headline matcher used by AI-authored tests to gate \"page loaded\" \
transitions. 5s default budget.",
"app.assert_visible(&id(\"lbl-dashboard-title\"))",
);
match app.assert_visible(&id("lbl-dashboard-title")).await {
Ok(()) => harness.record_pass("assert_visible", n, "dashboard title visible"),
Err(e) => harness.record_fail("assert_visible", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_assert_enabled(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard",
"btn-take-photo (camera-permission trigger)",
"assert_enabled() checks the matched element's `enabled` bool — \
distinct from visible (a disabled button is still visible). Test \
authors use it before tap() to disambiguate disabled-button vs \
element-not-found failures.",
"app.assert_enabled(&id(\"btn-take-photo\"))",
);
match app.assert_enabled(&id("btn-take-photo")).await {
Ok(()) => harness.record_pass("assert_enabled", n, "take-photo button enabled"),
Err(e) => harness.record_fail("assert_enabled", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_assert_text(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard",
"Literal text \"Welcome to smix selftest\"",
"assert_text() = assert_visible(&text(literal)). The fastest smoke \
check (no selector needed); great for \"is this the right screen?\" \
landmarks. Powered by the 6-field OR matcher under the hood.",
"app.assert_text(\"Welcome to smix selftest\")",
);
match app.assert_text("Welcome to smix selftest").await {
Ok(()) => harness.record_pass("assert_text", n, "welcome literal visible"),
Err(e) => harness.record_fail("assert_text", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_wait_for(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard → Delayed-visible probe",
"btn-show-delayed → lbl-delayed-visible (800ms timer)",
"wait_for() is the bounded-poll sense verb — re-evaluates the selector \
until match or timeout. The fixture renders lbl-delayed-visible 800ms \
after btn-show-delayed is tapped; we tap then wait up to 3s.",
"app.tap(&id(\"btn-show-delayed\")) → app.wait_for(&id(\"lbl-delayed-visible\"), 3s)",
);
if let Err(e) = app.tap(&id("btn-show-delayed")).await {
harness.record_fail(
"wait_for",
n,
&failure_kind_of(&e),
&format!("preparatory tap failed: {}", e.message),
None,
);
return;
}
match app
.wait_for(&id("lbl-delayed-visible"), Duration::from_secs(3))
.await
{
Ok(_) => harness.record_pass("wait_for", n, "delayed label appeared within 3s"),
Err(e) => harness.record_fail("wait_for", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_system_popup_action(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard → Camera permission alert",
"btn-take-photo → SpringBoard AVCaptureDevice permission popup",
"system_popup_action() is the G9 act side — taps a button on a \
previously enumerated iOS system popup. Sense + act close the loop on \
permission dialogs. If the privacy state has already granted camera \
access, the popup never appears and we record a Skipped instead of a \
false-positive Failed.",
"app.tap(&id(\"btn-take-photo\")) → system_popups() → system_popup_action(popup_id, button_id)",
);
if let Err(e) = app.tap(&id("btn-take-photo")).await {
harness.record_fail(
"system_popup_action",
n,
&failure_kind_of(&e),
&format!("trigger tap failed: {}", e.message),
None,
);
return;
}
let mut popups = Vec::new();
for _ in 0..6 {
tokio::time::sleep(Duration::from_millis(500)).await;
match app.system_popups().await {
Ok(p) if !p.is_empty() => {
popups = p;
break;
}
Ok(_) => continue,
Err(e) => {
harness.record_fail(
"system_popup_action",
n,
&failure_kind_of(&e),
&e.message,
None,
);
return;
}
}
}
let Some(popup) = popups.first() else {
harness.record_skip(
"system_popup_action",
n,
"no system popup surfaced (camera permission likely already granted on sim)",
);
return;
};
let button = popup
.buttons
.iter()
.find(|b| b.role != "cancel" && !b.dangerous)
.or_else(|| popup.buttons.first());
let Some(button) = button else {
harness.record_fail(
"system_popup_action",
n,
"PopupNoButtons",
&format!("popup {} has no buttons", popup.id),
None,
);
return;
};
match app.system_popup_action(&popup.id, &button.id).await {
Ok(true) => harness.record_pass(
"system_popup_action",
n,
&format!("tapped '{}' on popup {}", button.label, popup.id),
),
Ok(false) => harness.record_fail(
"system_popup_action",
n,
"StalePopupId",
"runner returned not_found — popup or button id stale",
None,
),
Err(e) => harness.record_fail(
"system_popup_action",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_scroll(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard list",
"card-9 (last card in the dashboard list — initially below the fold)",
"scroll() resolves a selector and drives the containing ScrollView so \
the matched element ends up in view. Distinct from swipe_once: \
scroll has a target and stops when the target is visible. Direction \
is CONTENT-scroll direction (maestro-compatible): Down = scroll \
deeper to reveal lower content (wire maps it to a swipe-up gesture).",
"app.scroll(&id(\"card-9\"), SwipeDirection::Down)",
);
match app.scroll(&id("card-9"), SwipeDirection::Down).await {
Ok(()) => harness.record_pass("scroll", n, "scroll-to card-9 dispatched"),
Err(e) => harness.record_fail("scroll", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_swipe_once(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home → Dashboard",
"Whole screen (no target — one swipe pass)",
"swipe_once() is the targetless variant — drives a single gesture in \
a direction without picking a destination element. Used for pull-to- \
refresh or carousel-page-flip patterns.",
"app.swipe_once(SwipeDirection::Down)",
);
match app.swipe_once(SwipeDirection::Down).await {
Ok(()) => harness.record_pass("swipe_once", n, "swipe down dispatched"),
Err(e) => harness.record_fail("swipe_once", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_tap_at_coord(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Home (any tab)",
"Normalized coord (0.5, 0.5) — viewport center",
"tap_at_coord() is the §9 #3 escape hatch — direct Apple-native-event \
-chain wire entry. Only authorized for yaml-port edge cases (maestro \
`point: \"X%,Y%\"`); selftest probes it at viewport center as a \
no-op coord that always exists.",
"app.tap_at_coord(0.5, 0.5)",
);
match app.tap_at_coord(0.5, 0.5).await {
Ok(()) => harness.record_pass("tap_at_coord", n, "center coord tap dispatched"),
Err(e) => harness.record_fail("tap_at_coord", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_open_url(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"Home → Settings (via custom URL scheme)",
"selftest://settings deeplink",
"open_url() routes through `simctl openurl` — the canonical deeplink \
test verb. The fixture registers `selftest://{home,search,alerts, \
settings}` to exercise this exact path.",
"app.open_url(\"selftest://settings\")",
);
let _ = app.launch(bundle).await;
tokio::time::sleep(Duration::from_millis(400)).await;
match app.open_url("selftest://settings").await {
Ok(()) => {
tokio::time::sleep(Duration::from_millis(800)).await;
harness.record_pass("open_url", n, "selftest://settings opened");
}
Err(e) => harness.record_fail("open_url", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_foreground(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"(transition — background then foreground)",
"SelftestFixture bundle (com.apple.springboard → fixture)",
"foreground() lifts a backgrounded app back to the front without \
re-launching. We background the fixture (launch SpringBoard) then \
foreground it; the fixture must come back to its prior screen state.",
"simctl.launch(udid, \"com.apple.springboard\") → app.foreground(bundle)",
);
if let Some(udid) = app.udid() {
let _ = app.simctl().launch(udid, "com.apple.springboard").await;
tokio::time::sleep(Duration::from_millis(600)).await;
}
match app.foreground(bundle).await {
Ok(()) => {
tokio::time::sleep(Duration::from_millis(600)).await;
harness.record_pass("foreground", n, "fixture brought back to front");
}
Err(e) => harness.record_fail("foreground", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_go_back(app: &App, harness: &mut Harness) {
let n = Narration::new(
"Settings → About → Settings",
"link-about (push DetailScreen) → back gesture",
"go_back() routes through the runner's NavigationStack-aware back \
dispatch — equivalent to swiping from the leading edge or tapping \
the system back chevron. We push About first so the back has \
somewhere to go.",
"app.tap(&id(\"link-about\")) → app.go_back()",
);
let _ = app.tap(&id("link-about")).await;
tokio::time::sleep(Duration::from_millis(600)).await;
match app.go_back().await {
Ok(()) => harness.record_pass("go_back", n, "back gesture dispatched"),
Err(e) => harness.record_fail("go_back", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_install(app: &App, harness: &mut Harness) {
let n = Narration::new(
"(no UI — error path probe)",
"Bogus .app path /nonexistent/smix-selftest-bogus.app",
"install() probe with a known-bad path. Capability is present iff the \
API returns a structured Err (DriverError / similar) instead of \
panicking. Selftest does not actually install the fixture — that's \
the example harness's job before scenario runs.",
"app.install(\"/nonexistent/smix-selftest-bogus.app\")",
);
match app.install("/nonexistent/smix-selftest-bogus.app").await {
Ok(()) => harness.record_fail(
"install",
n,
"UnexpectedOk",
"install of nonexistent path returned Ok (expected Err)",
None,
),
Err(e) => harness.record_pass(
"install",
n,
&format!(
"returned structured Err as expected ({})",
failure_kind_of(&e)
),
),
}
}
async fn seg_uninstall(app: &App, harness: &mut Harness) {
let n = Narration::new(
"(no UI — error path probe)",
"Bogus bundle id com.smix.selftest.does-not-exist",
"uninstall() probe with a known-bad bundle id. Capability check is \
the same shape as install: API must surface a structured Err rather \
than crash. We don't uninstall the real fixture — terminating it \
at the end of the run is enough.",
"app.uninstall(\"com.smix.selftest.does-not-exist\")",
);
match app.uninstall("com.smix.selftest.does-not-exist").await {
Ok(()) => harness.record_pass(
"uninstall",
n,
"simctl uninstall of unknown id reported success (Apple no-op)",
),
Err(e) => harness.record_pass(
"uninstall",
n,
&format!("returned structured Err ({})", failure_kind_of(&e)),
),
}
}
async fn seg_terminate(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"(transition — fixture teardown)",
"SelftestFixture (final clean shutdown)",
"terminate() at the end of the run leaves the sim in a known state \
for the next selftest run. Per [[smix-sim-recycle-after-use]] the \
outer cycle-close gate is responsible for shutting the sim itself.",
"app.terminate(bundle)",
);
match app.terminate(bundle).await {
Ok(()) => harness.record_pass("terminate", n, "fixture terminated cleanly"),
Err(e) => harness.record_fail("terminate", n, &failure_kind_of(&e), &e.message, None),
}
}
async fn seg_selector_text_regex_with_flags(app: &App, harness: &mut Harness) {
use smix_selector::{Modifiers, Pattern, Selector};
let n = Narration::new(
"Home dashboard",
"Dashboard heading text /welcome.*selftest/i",
"Pattern::regex_with_flags() is the case-insensitive escape hatch for \
dynamic copy that may switch casing across builds. Different from the \
default `text_regex()` (which auto-injects `i`) — this variant lets \
the caller pin flags explicitly, so the spec drill must exercise both.",
"app.find_one(Selector::Text { Pattern::regex_with_flags(\"welcome.*selftest\", \"i\") })",
);
let sel = Selector::Text {
text: Pattern::regex_with_flags("welcome.*selftest", "i"),
modifiers: Modifiers::default(),
};
match app.find_one(&sel).await {
Ok(Some(_)) => harness.record_selector_spec_pass(
"selector_text_regex_with_flags",
n,
"matched 'Welcome to smix selftest' on dashboard",
),
Ok(None) => harness.record_selector_spec_fail(
"selector_text_regex_with_flags",
n,
"NoMatch",
"regex_with_flags returned None on the dashboard heading",
None,
),
Err(e) => harness.record_selector_spec_fail(
"selector_text_regex_with_flags",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_selector_modifiers_spatial(app: &App, harness: &mut Harness) {
use smix_selector::{Modifiers, Selector};
let n = Narration::new(
"Home dashboard",
"Card 0 inside the dashboard ScrollView, above Card 5",
"Modifiers `ancestor` + `above` together: ancestor walks the a11y tree \
to require a ScrollView in the parent chain (structural filter); above \
requires the candidate to sit higher than card-5 on screen (spatial \
filter). Same Modifiers struct, two different filter regimes — the \
spec drill pins that compound resolves to a single candidate.",
"app.find_one(Selector::Id { id: card-0, ancestor: ScrollView, above: card-5 })",
);
let sel = Selector::Id {
id: "card-0".to_string(),
modifiers: Modifiers {
ancestor: Some(Box::new(role(Role::ScrollView))),
above: Some(Box::new(id("card-5"))),
..Default::default()
},
};
match app.find_one(&sel).await {
Ok(Some(_)) => harness.record_selector_spec_pass(
"selector_modifiers_spatial",
n,
"card-0 resolved with ancestor=ScrollView + above=card-5",
),
Ok(None) => harness.record_selector_spec_fail(
"selector_modifiers_spatial",
n,
"NoMatch",
"spatial+ancestor modifier filtered out card-0",
None,
),
Err(e) => harness.record_selector_spec_fail(
"selector_modifiers_spatial",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_selector_modifiers_index(app: &App, harness: &mut Harness) {
use smix_selector::{Modifiers, Selector};
let n = Narration::new(
"Home dashboard",
"Second Button (nth=1) anywhere on the dashboard",
"Modifiers `nth` indexes into the surviving candidate list (0-based). \
The Home dashboard surfaces exactly 2 Role::Button nodes \
(`btn-take-photo`, `btn-show-delayed`) — the tab-bar items render \
with rawType=button but the role deriver remaps them to Role::Tab \
(`smix_screen::derive_roles_recursive`). nth=1 picks the second \
non-tab button deterministically.",
"app.find_one(Selector::Role { Role::Button, nth: 1 })",
);
let sel = Selector::Role {
role: Role::Button,
name: None,
modifiers: Modifiers {
nth: Some(1),
..Default::default()
},
};
match app.find_one(&sel).await {
Ok(Some(_)) => harness.record_selector_spec_pass(
"selector_modifiers_index",
n,
"role(Button) nth=1 resolved on dashboard",
),
Ok(None) => harness.record_selector_spec_fail(
"selector_modifiers_index",
n,
"NoMatch",
"fewer than 2 Button nodes on dashboard for nth=1",
None,
),
Err(e) => harness.record_selector_spec_fail(
"selector_modifiers_index",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_expectation_failure_fallback(app: &App, harness: &mut Harness) {
const MISSING_ID: &str = "definitely-not-in-fixture-xyz";
let n = Narration::new(
"Home dashboard",
"tap on id `definitely-not-in-fixture-xyz` (intentional miss)",
"ExpectationFailure must carry AI-readable `message` + non-empty \
`visible_elements` so a failed selector resolves to a useful next-action \
hint instead of a stack trace. The spec drill pins that contract: the \
message names the missing target and the visible-elements list is \
populated from the live tree.",
"app.tap(&id(\"definitely-not-in-fixture-xyz\"))",
);
match app.tap(&id(MISSING_ID)).await {
Ok(()) => harness.record_selector_spec_fail(
"expectation_failure_fallback",
n,
"UnexpectedOk",
"tap on a deliberately-missing id returned Ok — fixture is too forgiving",
None,
),
Err(e) => {
let visible_ok = !e.visible_elements.is_empty();
let ai_readable_ok = !e.message.is_empty() && e.message.contains(MISSING_ID);
if visible_ok && ai_readable_ok {
harness.record_selector_spec_pass(
"expectation_failure_fallback",
n,
&format!(
"ExpectationFailure code={:?} visible_elements={} suggestions={} \
message_mentions_target=true",
e.code,
e.visible_elements.len(),
e.suggestions.len(),
),
);
} else {
harness.record_selector_spec_fail(
"expectation_failure_fallback",
n,
"FallbackThin",
&format!(
"expected non-empty visible_elements + target-mentioning message; \
got visible={} ai_readable={} (code={:?})",
e.visible_elements.len(),
ai_readable_ok,
e.code,
),
None,
);
}
}
}
}
async fn seg_maestro_parity_basic(app: &App, harness: &mut Harness) {
use std::time::Duration;
{
let n = Narration::new(
"post-Phase-6 Home Dashboard",
"viewport-center down-swipe",
"c1 §9 #3 swipe_at_coord lift — maestro swipe yaml parity probe",
"app.swipe_at_coord((0.5, 0.5), (0.5, 0.3))",
);
match app.swipe_at_coord((0.5, 0.5), (0.5, 0.3)).await {
Ok(()) => {
harness.record_maestro_parity_pass("swipe_at_coord", n, "swipe dispatched ok")
}
Err(e) => harness.record_maestro_parity_fail(
"swipe_at_coord",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"Home Dashboard",
"viewport down-scroll (one swipe)",
"c1 scroll_screen — maestro bare scroll yaml parity",
"app.scroll_screen(SwipeDirection::Down)",
);
match app.scroll_screen(crate::SwipeDirection::Down).await {
Ok(()) => {
harness.record_maestro_parity_pass("scroll_screen", n, "scroll dispatched ok")
}
Err(e) => harness.record_maestro_parity_fail(
"scroll_screen",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"Home Dashboard",
"selector id='never-exists-c7-probe-id-xyz'",
"c1 assert_not_visible — maestro assertNotVisible yaml parity",
"app.assert_not_visible(&id(\"never-exists-c7-probe-id-xyz\"))",
);
let sel = crate::id("never-exists-c7-probe-id-xyz");
match app.assert_not_visible(&sel).await {
Ok(()) => harness.record_maestro_parity_pass(
"assert_not_visible",
n,
"selector not visible (expected)",
),
Err(e) => harness.record_maestro_parity_fail(
"assert_not_visible",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"Home Dashboard",
"selector id='never-exists-c7-probe-id-xyz', timeout 500ms",
"c2 wait_for_not_visible — maestro extendedWaitUntil notVisible parity",
"app.wait_for_not_visible(&sel, 500ms)",
);
let sel = crate::id("never-exists-c7-probe-id-xyz");
match app
.wait_for_not_visible(&sel, Duration::from_millis(500))
.await
{
Ok(()) => harness.record_maestro_parity_pass(
"wait_for_not_visible",
n,
"selector not visible within timeout",
),
Err(e) => harness.record_maestro_parity_fail(
"wait_for_not_visible",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
let probe_text = "c7-probe-clipboard-text";
{
let n = Narration::new(
"device pasteboard",
"set value to 'c7-probe-clipboard-text'",
"c3 set_clipboard — maestro setClipboard yaml parity (simctl pbcopy)",
"app.set_clipboard(\"c7-probe-clipboard-text\")",
);
match app.set_clipboard(probe_text).await {
Ok(()) => {
harness.record_maestro_parity_pass("set_clipboard", n, "clipboard written ok")
}
Err(e) => harness.record_maestro_parity_fail(
"set_clipboard",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"device pasteboard",
"read pasteboard (expect 'c7-probe-clipboard-text')",
"c3 get_clipboard — maestro pasteText bare-form helper",
"app.get_clipboard()",
);
match app.get_clipboard().await {
Ok(value) if value.trim() == probe_text => harness.record_maestro_parity_pass(
"get_clipboard",
n,
"clipboard readback matches probe text",
),
Ok(value) => harness.record_maestro_parity_fail(
"get_clipboard",
n,
"AssertionFailed",
&format!("readback mismatch: got {value:?}, expected {probe_text:?}"),
None,
),
Err(e) => harness.record_maestro_parity_fail(
"get_clipboard",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"sim device GPS",
"set to San Francisco (37.7749, -122.4194)",
"c5 set_location — maestro setLocation yaml parity (simctl location set)",
"app.set_location(37.7749, -122.4194)",
);
match app.set_location(37.7749, -122.4194).await {
Ok(()) => harness.record_maestro_parity_pass("set_location", n, "location set ok"),
Err(e) => harness.record_maestro_parity_fail(
"set_location",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"sim device GPS",
"interpolate two waypoints fire-and-return",
"c5 travel — maestro travel yaml parity (simctl location start)",
"app.travel(&[(37.7, -122.4), (37.8, -122.5)], None)",
);
let pts = [(37.7_f64, -122.4_f64), (37.8_f64, -122.5_f64)];
match app.travel(&pts, None).await {
Ok(()) => harness.record_maestro_parity_pass(
"travel",
n,
"travel scenario injected ok (fire-and-return)",
),
Err(e) => harness.record_maestro_parity_fail(
"travel",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"sim device orientation",
"rotate to portrait",
"c5 set_orientation — maestro setOrientation yaml parity (swift XCUIDevice)",
"app.set_orientation(MaestroOrientation::Portrait)",
);
match app
.set_orientation(crate::MaestroOrientation::Portrait)
.await
{
Ok(()) => harness.record_maestro_parity_pass(
"set_orientation",
n,
"orientation set to portrait ok",
),
Err(e) => harness.record_maestro_parity_fail(
"set_orientation",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
{
let n = Narration::new(
"Home Dashboard",
"dhash 64-bit against baseline at tmp_path (auto-record first run)",
"c6 assert_screenshot — maestro assertScreenshot yaml parity (dhash perceptual)",
"app.assert_screenshot(&tmp_baseline_path, 5)",
);
let mut baseline = std::env::temp_dir();
baseline.push(format!(
"smix-c7-baseline-{}-{}.png",
std::process::id(),
harness
.run_dir()
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("nodir")
));
let _ = std::fs::remove_file(&baseline);
match app.assert_screenshot(&baseline, 5).await {
Ok(crate::AssertScreenshotOutcome::Recorded { .. }) => {
harness.record_maestro_parity_pass(
"assert_screenshot",
n,
"baseline auto-recorded (first run path)",
);
}
Ok(crate::AssertScreenshotOutcome::Matched { hamming }) => {
harness.record_maestro_parity_pass(
"assert_screenshot",
n,
&format!("baseline matched (hamming={hamming})"),
);
}
Err(e) => harness.record_maestro_parity_fail(
"assert_screenshot",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
let _ = std::fs::remove_file(&baseline);
}
let skips: &[(&str, &str)] = &[
(
"set_permission",
"permission state spill into Phase 6/7 teardown; v5.3+ isolate fixture",
),
(
"set_permissions",
"batch permission setter requires bundle isolation; v5.3+",
),
(
"launch_app_with_options",
"deferred — would restart SelftestFixture mid-scenario; v5.3+ separate scenario",
),
(
"paste_text",
"deferred — requires keyboard-focused input element on fixture",
),
(
"copy_text_from",
"deferred — needs deterministic a11y text element on Home dashboard",
),
(
"double_tap",
"deferred — requires selector for known-double-tappable button",
),
(
"long_press",
"deferred — requires selector for known-long-pressable element",
),
(
"add_media",
"deferred — needs PNG asset checked into fixtures",
),
(
"start_recording",
"deferred — long-running child process state across scenario phases",
),
(
"stop_recording",
"deferred — paired with start_recording above",
),
];
for (name, reason) in skips {
let n = Narration::new(
"(skipped — deferred to v5.3+)",
"n/a",
"v5.2 c7 explicit defer; documented v5.3+ work",
"skipped",
);
harness.record_maestro_parity_skip(name, n, reason);
}
}
fn count_nodes(node: &smix_screen::A11yNode) -> usize {
let mut n = 1;
for child in &node.children {
n += count_nodes(child);
}
n
}
async fn v2_enter(app: &App, bundle: &str) -> Result<(), ExpectationFailure> {
let _ = app.terminate(bundle).await;
tokio::time::sleep(Duration::from_millis(400)).await;
app.launch(bundle).await?;
tokio::time::sleep(Duration::from_millis(800)).await;
app.tap_xcui("v2-jump-btn").await?;
tokio::time::sleep(Duration::from_millis(1200)).await;
Ok(())
}
async fn seg_v2_home_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 home tab — counter increment/reset",
"v2-home-counter-label / v2-home-increment-btn / v2-home-reset-btn",
"v5.3 c4 SDK 自家路径 e2e 等价 c2 yaml 01_home_increment_reset",
"v2_enter → tap home tab → +1 ×3 → reset → assert counter",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-home")).await?;
app.assert_visible(&id("v2-home-counter-label")).await?;
app.tap(&id("v2-home-increment-btn")).await?;
app.tap(&id("v2-home-increment-btn")).await?;
app.tap(&id("v2-home-increment-btn")).await?;
app.assert_visible(&id("v2-home-counter-label")).await?;
app.tap(&id("v2-home-reset-btn")).await?;
app.assert_visible(&id("v2-home-counter-label")).await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass("v2_home_basic", n, "home tab counter cycle ok"),
Err(e) => {
harness.record_v2_e2e_fail("v2_home_basic", n, &failure_kind_of(&e), &e.message, None)
}
}
}
async fn seg_v2_form_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 form tab — fill 3 fields + submit",
"v2-form-{name,email,password}-input / v2-form-submit-btn",
"v5.3 c4 SDK 自家路径 e2e 等价 c2 yaml 02_form_fill_submit",
"v2_enter → tap form tab → fill ×3 → submit → assert submitted",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-form")).await?;
app.tap(&id("v2-form-name-input")).await?;
app.fill(&focused(), "Alice").await?;
app.tap(&id("v2-form-email-input")).await?;
app.fill(&focused(), "alice@example.com").await?;
app.tap(&id("v2-form-password-input")).await?;
app.fill(&focused(), "s3cret").await?;
app.hide_keyboard().await?;
app.tap(&id("v2-form-submit-btn")).await?;
app.assert_visible(&id("v2-form-submitted-label")).await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass("v2_form_basic", n, "form submit + label ok"),
Err(e) => {
harness.record_v2_e2e_fail("v2_form_basic", n, &failure_kind_of(&e), &e.message, None)
}
}
}
async fn seg_v2_list_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 list tab — scroll until row-50 visible",
"v2-list-row-0 / v2-list-row-50",
"v5.3 c4 SDK 自家路径 e2e 等价 c2 yaml 03_list_scroll",
"v2_enter → tap list tab → scroll(v2-list-row-50, Down) → assert",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-list")).await?;
app.assert_visible(&id("v2-list-row-0")).await?;
app.scroll(&id("v2-list-row-50"), SwipeDirection::Down)
.await?;
app.assert_visible(&id("v2-list-row-50")).await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass("v2_list_basic", n, "scroll-until-visible row-50 ok"),
Err(e) => {
harness.record_v2_e2e_fail("v2_list_basic", n, &failure_kind_of(&e), &e.message, None)
}
}
}
async fn seg_v2_modal_sheet_basic(_app: &App, harness: &mut Harness, _bundle: &str) {
let n = Narration::new(
"V2 modal tab — .sheet open + dismiss (deferred)",
"v2-modal-open-sheet-btn / v2-modal-sheet-dismiss-btn",
"v5.3 c4 explicit skip — SwiftUI .sheet dismiss tap is (c) v5.4+ backlog",
"skipped: maestro-CLI baseline still passes; smix tap_xcui doesn't fire binding",
);
harness.record_v2_e2e_skip(
"v2_modal_sheet_basic",
n,
"deferred to v5.4+ (c) backlog: SwiftUI .sheet dismiss binding does not fire from XCUIElement.tap() / XCUICoordinate.tap() on iOS 17+; runner reports tap dispatched but onTap closure stays cold. maestro CLI passes the same flow via a yet-unidentified internal path; root cause requires swift sim-side investigation outside cycle scope. tag v5.x-backlog-c4.",
);
}
async fn seg_v2_modal_alert_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 modal tab — .alert open + OK",
"v2-modal-open-alert-btn / v2-modal-alert-ok-btn",
"v5.13 c1 SDK 自家路径 e2e 等价 c2 yaml 05_modal_alert_ok (v5.12 c1 IOHID handler 解锁)",
"v2_enter → tap modal tab → open alert → tap_xcui OK → wait_for_not_visible",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-modal").await?;
app.tap_xcui("v2-modal-open-alert-btn").await?;
app.assert_visible(&id("v2-modal-alert-ok-btn")).await?;
app.tap_xcui("v2-modal-alert-ok-btn").await?;
app.wait_for_not_visible(&id("v2-modal-alert-ok-btn"), Duration::from_millis(3000))
.await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_modal_alert_basic",
n,
".alert OK tap fires SwiftUI binding via IOHID (v5.12 c1 handler)",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_modal_alert_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_modal_actionsheet_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 modal tab — .confirmationDialog Choice A",
"v2-modal-open-actionsheet-btn / v2-modal-action-a-btn",
"v5.13 c1 SDK 自家路径 e2e 等价 c2 yaml 06_modal_actionsheet_cancel (v5.12 c1 IOHID handler 解锁)",
"v2_enter → tap modal tab → open actionsheet → tap_xcui Choice A → wait_for_not_visible",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-modal").await?;
app.tap_xcui("v2-modal-open-actionsheet-btn").await?;
app.assert_visible(&id("v2-modal-action-a-btn")).await?;
app.tap_xcui("v2-modal-action-a-btn").await?;
app.wait_for_not_visible(&id("v2-modal-action-a-btn"), Duration::from_millis(3000))
.await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_modal_actionsheet_basic",
n,
".confirmationDialog Choice A tap fires SwiftUI binding via IOHID (v5.12 c1 handler)",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_modal_actionsheet_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_modal_fullscreen_basic(_app: &App, harness: &mut Harness, _bundle: &str) {
let n = Narration::new(
"V2 modal tab — .fullScreenCover dismiss (deferred)",
"v2-modal-open-fullscreen-btn / v2-modal-fullscreen-dismiss-btn",
"v5.3 c4 explicit skip — SwiftUI .fullScreenCover dismiss is (c) v5.4+ backlog",
"skipped: maestro-CLI baseline still passes; smix tap_xcui doesn't fire binding",
);
harness.record_v2_e2e_skip(
"v2_modal_fullscreen_basic",
n,
"deferred to v5.4+ (c) backlog: same swift-modal-hit-target root cause as v2_modal_sheet_basic. tag v5.x-backlog-c4.",
);
}
async fn seg_v2_clipboard_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 clip tab — copy / paste fixture-self-loop",
"v2-clip-copy-btn / v2-clip-paste-btn / v2-clip-content-label",
"v5.3 c4 SDK 自家路径 e2e 等价 c2 yaml 08_clipboard_copy_paste",
"v2_enter → tap clip tab → copy → paste → assert content-label",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-clip")).await?;
app.tap(&id("v2-clip-copy-btn")).await?;
app.tap(&id("v2-clip-paste-btn")).await?;
app.assert_visible(&id("v2-clip-content-label")).await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass("v2_clipboard_basic", n, "clipboard self-loop ok"),
Err(e) => harness.record_v2_e2e_fail(
"v2_clipboard_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_keyboard_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 kbd tab — focus + type + Enter + hide_keyboard",
"v2-kbd-focus-btn / v2-kbd-text-input / v2-kbd-submitted-label",
"v5.3 c4 SDK 自家路径 e2e 等价 c2 yaml 09_keyboard_type_hide",
"v2_enter → tap kbd tab → focus → fill hello → press Enter → assert label",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-kbd")).await?;
app.tap(&id("v2-kbd-focus-btn")).await?;
app.fill(&focused(), "hello").await?;
app.press_key(KeyName::Return).await?;
app.assert_visible(&id("v2-kbd-submitted-label")).await?;
app.hide_keyboard().await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_keyboard_basic",
n,
"keyboard focus + fill + Enter + hide ok",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_keyboard_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_orientation_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 orient tab — landscape ↔ portrait rotate",
"v2-orient-current-label",
"v5.3 c4 SDK 自家路径 e2e 等价 c2 yaml 10_orientation_rotate",
"v2_enter → tap orient → set_orientation LandscapeLeft + Portrait + wait",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-orient")).await?;
app.assert_visible(&id("v2-orient-current-label")).await?;
app.set_orientation(crate::MaestroOrientation::LandscapeLeft)
.await?;
app.wait_for(&id("v2-orient-current-label"), Duration::from_secs(4))
.await?;
app.set_orientation(crate::MaestroOrientation::Portrait)
.await?;
app.wait_for(&id("v2-orient-current-label"), Duration::from_secs(4))
.await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_orientation_basic",
n,
"set_orientation 双向 round-trip ok",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_orientation_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_location_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 loc tab — set_location + request → coord label visible",
"v2-loc-request-btn / v2-loc-coord-label",
"v5.3 c4 SDK 自家路径 e2e 等价 c2 yaml 11_location_mock",
"v2_enter → tap loc → set_location(Tokyo) → tap request → wait_for coord",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-loc")).await?;
app.set_location(35.6764, 139.6500).await?;
app.tap(&id("v2-loc-request-btn")).await?;
app.wait_for(&id("v2-loc-coord-label"), Duration::from_secs(8))
.await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => {
harness.record_v2_e2e_pass("v2_location_basic", n, "set_location + request + coord ok")
}
Err(e) => harness.record_v2_e2e_fail(
"v2_location_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_permission_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 perm tab — camera SpringBoard popup full loop",
"v2-perm-camera-btn / SpringBoard alert / v2-perm-status-label",
"v5.5 c3 SDK 自家路径 e2e 等价 maestro yaml 14_permission_camera",
"set_permission(Reset) → v2_enter → tap perm tab → tap camera-btn → \
poll system_popups → tap Allow → assert status label camera=granted",
);
let result = async {
app.set_permission(bundle, SimctlPermission::Camera, PermissionAction::Reset)
.await?;
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-perm").await?;
app.assert_visible(&id("v2-perm-camera-btn")).await?;
app.tap(&id("v2-perm-camera-btn")).await?;
let mut popups = Vec::new();
for _ in 0..12 {
tokio::time::sleep(Duration::from_millis(500)).await;
let p = app.system_popups().await?;
if !p.is_empty() {
popups = p;
break;
}
}
let Some(popup) = popups.first() else {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message:
"SpringBoard camera permission popup did not surface within 6s after reset+tap"
.into(),
..Default::default()
}));
};
let button = popup
.buttons
.iter()
.find(|b| b.role != "cancel" && !b.dangerous)
.or_else(|| popup.buttons.first())
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "popup surfaced but has no actionable button".into(),
..Default::default()
})
})?;
let popup_id = popup.id.clone();
let button_id = button.id.clone();
let ok = app.system_popup_action(&popup_id, &button_id).await?;
if !ok {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "system_popup_action returned not_found — popup/button id stale".into(),
..Default::default()
}));
}
app.wait_for(&id("v2-perm-status-label"), Duration::from_secs(4))
.await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_permission_basic",
n,
"reset + popup + tap Allow + status label ok",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_permission_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_recording_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 rec tab — start_recording + stop_recording 长跑 mp4 capture",
"v2-rec-status-label / simctl io recordVideo child",
"v5.5 c4 SDK 自家路径 e2e — long-running child + SIGINT lifecycle",
"v2_enter → tap rec tab → start_recording(tmp.mp4) → sleep 3s → stop_recording → assert mp4 size",
);
let mut mp4 = std::env::temp_dir();
mp4.push(format!(
"smix-v5.5-c4-rec-{}-{}.mp4",
std::process::id(),
harness
.run_dir()
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("nodir")
));
let _ = std::fs::remove_file(&mp4);
let mp4_str = mp4.to_string_lossy().to_string();
let result = async {
v2_enter(app, bundle).await?;
app.tap(&id("v2-tab-rec")).await?;
app.assert_visible(&id("v2-rec-status-label")).await?;
app.start_recording(&mp4_str).await?;
tokio::time::sleep(Duration::from_secs(3)).await;
app.stop_recording().await?;
let size = std::fs::metadata(&mp4)
.map(|m| m.len())
.map_err(|e| ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"stat on recorded mp4 {} failed: {} — recorder did not write the file (was capsule started without --no-capture? see v5.6 c1/c2 root cause)",
mp4.display(), e
),
..Default::default()
}))?;
if size < 4_096 {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"recorded mp4 too small ({} bytes < 4096) — recorder likely captured zero frames",
size
),
..Default::default()
}));
}
Ok::<u64, ExpectationFailure>(size)
}
.await;
let cleanup = std::fs::remove_file(&mp4);
match result {
Ok(size) => harness.record_v2_e2e_pass(
"v2_recording_basic",
n,
&format!("recordVideo round-trip ok: {size} bytes captured over 3s"),
),
Err(e) => {
let _ = cleanup;
harness.record_v2_e2e_fail(
"v2_recording_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
)
}
}
}
async fn seg_v2_argv_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 argv tab — launch_app_with_options ships argv to SwiftUI",
"v2-argv-count-label / v2-argv-joined-label",
"v5.6 c4 SDK 自家路径 e2e — launchApp.arguments mapping-form 透传 SwiftUI ProcessInfo",
"launch_app_with_options(arguments=[-customKey, MyValue]) → tap v2-tab-argv → find_text v2-argv-joined-label → assert contains -customKey + MyValue",
);
let custom_key = "-smixV2ArgvProbe";
let custom_val = "smix-v5.6-c4-probe-value";
let opts = crate::LaunchAppOptions {
bundle_id: bundle.to_string(),
clear_state: false,
clear_keychain: false,
arguments: vec![custom_key.to_string(), custom_val.to_string()],
permissions: Vec::new(),
app_path: None,
};
let result = async {
app.launch_app_with_options(&opts).await?;
tokio::time::sleep(Duration::from_millis(1200)).await;
app.tap_xcui("v2-jump-btn").await?;
tokio::time::sleep(Duration::from_millis(1200)).await;
app.tap_xcui("v2-tab-argv").await?;
app.assert_visible(&id("v2-argv-count-label")).await?;
let joined_node = app
.find_one(&id("v2-argv-joined-label"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-argv-joined-label not found in a11y tree".into(),
..Default::default()
})
})?;
let mut all_fields = String::new();
if let Some(s) = &joined_node.label {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &joined_node.title {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &joined_node.value {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &joined_node.text {
all_fields.push_str(s);
all_fields.push(' ');
}
if !all_fields.contains(custom_key) {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-argv-joined-label does not contain `{custom_key}`; \
fields read: label={:?} title={:?} value={:?} text={:?}",
joined_node.label, joined_node.title, joined_node.value, joined_node.text
),
..Default::default()
}));
}
if !all_fields.contains(custom_val) {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-argv-joined-label does not contain `{custom_val}`; \
fields read: label={:?} title={:?} value={:?} text={:?}",
joined_node.label, joined_node.title, joined_node.value, joined_node.text
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_argv_basic",
n,
"launch_app_with_options argv pair surfaced to SwiftUI ProcessInfo",
),
Err(e) => {
harness.record_v2_e2e_fail("v2_argv_basic", n, &failure_kind_of(&e), &e.message, None)
}
}
}
async fn seg_v2_deeplink_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 deeplink tab — simctl openurl threads through .onOpenURL",
"v2-deeplink-target-label / v2-deeplink-event-count-label",
"v5.7 c1 SDK 自家路径 e2e — custom URL scheme deeplink 到 SwiftUI @Published 透传",
"launch → open_url(selftest://v2/deeplink-probe?key=...) → tap v2-jump-btn → tap v2-tab-deeplink → assert target-label contains expected URL tokens",
);
let probe_url = "selftest://v2/deeplink-probe?key=v5.7-c1-probe";
let probe_token_a = "selftest://v2/deeplink-probe";
let probe_token_b = "key=v5.7-c1-probe";
let opts = crate::LaunchAppOptions {
bundle_id: bundle.to_string(),
clear_state: false,
clear_keychain: false,
arguments: Vec::new(),
permissions: Vec::new(),
app_path: None,
};
let result = async {
app.launch_app_with_options(&opts).await?;
tokio::time::sleep(Duration::from_millis(800)).await;
app.tap_xcui("v2-jump-btn").await?;
tokio::time::sleep(Duration::from_millis(1200)).await;
app.open_url(probe_url).await?;
let mut popups = Vec::new();
for _ in 0..12 {
tokio::time::sleep(Duration::from_millis(500)).await;
let p = app.system_popups().await?;
if !p.is_empty() {
popups = p;
break;
}
}
if let Some(popup) = popups.first() {
let button = popup
.buttons
.iter()
.find(|b| b.role != "cancel" && !b.dangerous)
.or_else(|| popup.buttons.first())
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "openurl popup surfaced but has no actionable button".into(),
..Default::default()
})
})?;
let _ = app.system_popup_action(&popup.id, &button.id).await?;
}
tokio::time::sleep(Duration::from_millis(800)).await;
app.tap(&id("v2-tab-deeplink")).await?;
app.assert_visible(&id("v2-deeplink-target-label")).await?;
let target_node = app
.find_one(&id("v2-deeplink-target-label"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-deeplink-target-label not found in a11y tree".into(),
..Default::default()
})
})?;
let mut all_fields = String::new();
if let Some(s) = &target_node.label {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.title {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.value {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.text {
all_fields.push_str(s);
all_fields.push(' ');
}
if !all_fields.contains(probe_token_a) {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-deeplink-target-label does not contain `{probe_token_a}`; \
fields read: label={:?} title={:?} value={:?} text={:?}",
target_node.label, target_node.title, target_node.value, target_node.text
),
..Default::default()
}));
}
if !all_fields.contains(probe_token_b) {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-deeplink-target-label does not contain `{probe_token_b}`; \
fields read: label={:?} title={:?} value={:?} text={:?}",
target_node.label, target_node.title, target_node.value, target_node.text
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_deeplink_basic",
n,
"simctl openurl threaded custom URL through SwiftUI .onOpenURL to @Published state",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_deeplink_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_share_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 share tab — UIActivityViewController present + auto-dismiss",
"v2-share-trigger-btn / v2-share-last-result-label",
"v5.7 c2 SDK 自家路径 e2e — UIKit share sheet present 验证 (fixture-owned auto-dismiss)",
"v2_enter → tap v2-tab-share → assert_visible v2-share-trigger-btn → tap_xcui v2-share-trigger-btn → wait_for v2-share-last-result-label → assert label contains 'triggered' or 'cancelled' token",
);
let probe_token = "v5.7-c2 share probe text";
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-share").await?;
app.assert_visible(&id("v2-share-trigger-btn")).await?;
app.mark_fixture_action("v2-share-present");
app.tap_xcui("v2-share-trigger-btn").await?;
tokio::time::sleep(Duration::from_millis(800)).await;
let target_node = app
.find_one(&id("v2-share-last-result-label"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-share-last-result-label not found in a11y tree".into(),
..Default::default()
})
})?;
let mut all_fields = String::new();
if let Some(s) = &target_node.label {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.title {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.value {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.text {
all_fields.push_str(s);
all_fields.push(' ');
}
let saw_trigger = all_fields.contains("triggered") && all_fields.contains(probe_token);
let saw_completion = all_fields.contains("cancelled:") || all_fields.contains("completed:");
if !saw_trigger && !saw_completion {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-share-last-result-label missing both `triggered:{probe_token}` and \
`cancelled:|completed:` tokens; fields read: label={:?} title={:?} \
value={:?} text={:?}",
target_node.label, target_node.title, target_node.value, target_node.text
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_share_basic",
n,
"UIActivityViewController present + fixture auto-dismiss handler fired",
),
Err(e) => {
harness.record_v2_e2e_fail("v2_share_basic", n, &failure_kind_of(&e), &e.message, None)
}
}
}
async fn seg_v2_push_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 push tab — UNUserNotificationCenter delegate receives simctl push",
"v2-push-request-auth-btn / v2-push-status-label / v2-push-last-payload-label",
"v5.7 c3 SDK 自家路径 e2e — simctl push 透传 APNS JSON → UNUserNotificationCenterDelegate → @Published store",
"v2_enter → tap v2-tab-push → tap request-auth-btn → poll system_popups + tap Allow → assert status=authorized → app.send_push(bundle, apns.json) → assert payload-label contains apsBody token",
);
let probe_body = "v5.7-c3-push-probe-body";
let probe_user_info_key = "smixProbe";
let apns_path = "selftest-fixture/maestro-e2e/_push_probe.apns";
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-push").await?;
app.assert_visible(&id("v2-push-status-label")).await?;
app.mark_fixture_action("v2-push-request-auth");
app.tap_xcui("v2-push-request-auth-btn").await?;
let mut popups = Vec::new();
for _ in 0..12 {
tokio::time::sleep(Duration::from_millis(500)).await;
let p = app.system_popups().await?;
if !p.is_empty() {
popups = p;
break;
}
}
if let Some(popup) = popups.first() {
let button = popup
.buttons
.iter()
.find(|b| b.role != "cancel" && !b.dangerous)
.or_else(|| popup.buttons.first())
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "notification permission popup surfaced but has no actionable button".into(),
..Default::default()
})
})?;
let _ = app.system_popup_action(&popup.id, &button.id).await?;
tokio::time::sleep(Duration::from_millis(600)).await;
}
let _ = app
.wait_for(&id("v2-push-status-label"), Duration::from_secs(3))
.await;
app.send_push(bundle, apns_path).await?;
tokio::time::sleep(Duration::from_millis(1500)).await;
let target_node = app
.find_one(&id("v2-push-last-payload-label"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-push-last-payload-label not found in a11y tree".into(),
..Default::default()
})
})?;
let mut all_fields = String::new();
if let Some(s) = &target_node.label {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.title {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.value {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.text {
all_fields.push_str(s);
all_fields.push(' ');
}
if !all_fields.contains(probe_body) {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-push-last-payload-label does not contain `{probe_body}`; \
fields read: label={:?} title={:?} value={:?} text={:?}",
target_node.label, target_node.title, target_node.value, target_node.text
),
..Default::default()
}));
}
if !all_fields.contains(probe_user_info_key) {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-push-last-payload-label does not contain custom userInfo key `{probe_user_info_key}`; \
fields read: label={:?} title={:?} value={:?} text={:?}",
target_node.label, target_node.title, target_node.value, target_node.text
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_push_basic",
n,
"simctl push delivered APNS to UNUserNotificationCenter delegate; probe token surfaced to @Published store",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_push_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_picker_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 picker tab — UIDocumentPickerViewController present + fixture-owned dismiss",
"v2-picker-trigger-btn / v2-picker-last-result-label",
"v5.7 c4 SDK 自家路径 e2e — Files.app picker present 验证 (fixture-owned auto-dismiss + UIDocumentPickerDelegate cancel path)",
"v2_enter → tap v2-tab-picker → assert_visible v2-picker-trigger-btn → tap_xcui v2-picker-trigger-btn → wait_for v2-picker-last-result-label → assert label contains 'triggered' or 'cancelled' token",
);
let probe_token = "plainText|json";
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-picker").await?;
app.assert_visible(&id("v2-picker-trigger-btn")).await?;
app.mark_fixture_action("v2-picker-present");
app.tap_xcui("v2-picker-trigger-btn").await?;
tokio::time::sleep(Duration::from_millis(800)).await;
let target_node = app
.find_one(&id("v2-picker-last-result-label"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-picker-last-result-label not found in a11y tree".into(),
..Default::default()
})
})?;
let mut all_fields = String::new();
if let Some(s) = &target_node.label {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.title {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.value {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.text {
all_fields.push_str(s);
all_fields.push(' ');
}
let saw_trigger = all_fields.contains("triggered") && all_fields.contains(probe_token);
let saw_completion = all_fields.contains("cancelled") || all_fields.contains("selected:");
if !saw_trigger && !saw_completion {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-picker-last-result-label missing both `triggered:{probe_token}` and \
`cancelled|selected:` tokens; fields read: label={:?} title={:?} \
value={:?} text={:?}",
target_node.label, target_node.title, target_node.value, target_node.text
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_picker_basic",
n,
"UIDocumentPickerViewController present + fixture auto-dismiss delegate fired",
),
Err(e) => {
harness.record_v2_e2e_fail("v2_picker_basic", n, &failure_kind_of(&e), &e.message, None)
}
}
}
async fn seg_v2_auth_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 auth tab — ASAuthorizationController Sign-in-with-Apple wire",
"v2-auth-trigger-btn / v2-auth-last-result-label",
"v5.7 c5 SDK 自家路径 e2e — Sign in with Apple wire (sim 无 iCloud 帐号期望 error path, success path 同 record 兼容)",
"v2_enter → tap v2-tab-auth → assert_visible v2-auth-trigger-btn → tap_xcui v2-auth-trigger-btn → wait_for v2-auth-last-result-label → assert label contains 'triggered:authrequest' or 'error:' / 'success:' token",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-auth").await?;
app.assert_visible(&id("v2-auth-trigger-btn")).await?;
app.mark_fixture_action("v2-auth-present");
app.tap_xcui("v2-auth-trigger-btn").await?;
tokio::time::sleep(Duration::from_millis(1200)).await;
let target_node = app
.find_one(&id("v2-auth-last-result-label"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-auth-last-result-label not found in a11y tree".into(),
..Default::default()
})
})?;
let mut all_fields = String::new();
if let Some(s) = &target_node.label {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.title {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.value {
all_fields.push_str(s);
all_fields.push(' ');
}
if let Some(s) = &target_node.text {
all_fields.push_str(s);
all_fields.push(' ');
}
let saw_trigger = all_fields.contains("triggered:authrequest");
let saw_completion = all_fields.contains("error:") || all_fields.contains("success:");
if !saw_trigger && !saw_completion {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!(
"v2-auth-last-result-label missing both `triggered:authrequest` and \
`error:|success:` tokens; fields read: label={:?} title={:?} \
value={:?} text={:?}",
target_node.label, target_node.title, target_node.value, target_node.text
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_auth_basic",
n,
"ASAuthorizationController Sign-in-with-Apple wire fired; delegate token surfaced to @Published store",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_auth_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_anchor_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 anchor-relative coord — L6 sense layer (find_norm_coord)",
"v2-anchor-status-label / v2-anchor-last-tapped",
"v5.20 c1 SDK 自家路径 e2e — find anchor centroid in normalized [0,1] viewport coords; \
no separate tap performed (yaml 23 covers the full anchored: → tap_at_coord dispatch \
via adapter).",
"v2_enter → tap v2-tab-anchor → assert_visible status label → \
find_norm_coord(id v2-anchor-status-label) → expect Some(nx, ny) with nx in (0,1), ny in (0,1)",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-anchor").await?;
app.assert_visible(&id("v2-anchor-status-label")).await?;
let coord = app.find_norm_coord(&id("v2-anchor-status-label")).await?;
match coord {
Some((nx, ny)) if nx > 0.0 && nx < 1.0 && ny > 0.0 && ny < 1.0 => {
Ok::<(f64, f64), ExpectationFailure>((nx, ny))
}
Some((nx, ny)) => Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!("find_norm_coord returned ({nx}, {ny}) outside (0,1)"),
..Default::default()
})),
None => Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "find_norm_coord returned None for v2-anchor-status-label".into(),
..Default::default()
})),
}
}
.await;
match result {
Ok((nx, ny)) => harness.record_v2_e2e_pass(
"v2_anchor_basic",
n,
&format!(
"find_norm_coord -> ({:.3}, {:.3}) in viewport [0,1]",
nx, ny
),
),
Err(e) => {
harness.record_v2_e2e_fail("v2_anchor_basic", n, &failure_kind_of(&e), &e.message, None)
}
}
}
async fn seg_v2_ocr_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 ocr tab — Apple Vision OCR sense layer (find_by_text_ocr + tap_by_text_ocr)",
"v2-ocr-last-tapped",
"v5.19 c1 SDK 自家路径 e2e — Vision recognizes labels on V2OcrScreen; tap_by_text_ocr \
taps via IOHID synthesize at OCR centroid; result label updates.",
"v2_enter → tap v2-tab-ocr → find_by_text_ocr(\"Submit\") expect Some(frame) → \
tap_by_text_ocr(\"Submit\") → wait label contains 'tapped: Submit'",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-ocr").await?;
app.assert_visible(&id("v2-ocr-last-tapped")).await?;
let locales: Vec<String> = vec!["en".into()];
let frame = app.find_by_text_ocr("Submit", &locales).await?;
let frame = frame.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "find_by_text_ocr returned None for 'Submit'".into(),
..Default::default()
})
})?;
if frame.w <= 0.0 || frame.h <= 0.0 {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: format!("find_by_text_ocr returned empty frame: {:?}", frame),
..Default::default()
}));
}
app.tap_by_text_ocr("Submit", &locales).await?;
tokio::time::sleep(Duration::from_millis(800)).await;
let label_node = app
.find_one(&id("v2-ocr-last-tapped"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-ocr-last-tapped not found post-tap".into(),
..Default::default()
})
})?;
let label_text = label_node
.text
.as_deref()
.or(label_node.value.as_deref())
.or(label_node.label.as_deref())
.unwrap_or("");
if !label_text.contains("Submit") {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::AssertionFailed),
message: format!(
"v2-ocr-last-tapped did not show 'Submit' tap; got {label_text:?}"
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_ocr_basic",
n,
"Apple Vision OCR matched 'Submit'; tap_by_text_ocr fired via IOHID synthesize; result label updated",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_ocr_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_v2_webview_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 webview tab — WKWebView eval JS via SmixWebViewBridge (webview_eval)",
"v2-webview-title-label / WKWebView DOM",
"v5.21 c1b SDK 自家路径 e2e — bridge running on 127.0.0.1:28080, JS eval round-trip \
returns serialized DOM values (button textContent + DOM mutation + click + read).",
"v2_enter → tap v2-tab-webview → assert_visible title → \
webview_eval(\"document.getElementById('submit-form-btn').textContent\") expect 'Submit' → \
webview_eval(set input + click submit + read result) expect 'submitted: hello'",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-webview").await?;
app.assert_visible(&id("v2-webview-title-label")).await?;
tokio::time::sleep(Duration::from_millis(1200)).await;
let btn_text = app
.webview_eval("document.getElementById('submit-form-btn').textContent")
.await?;
let btn_text_str = btn_text.as_str().unwrap_or("");
if btn_text_str != "Submit" {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::AssertionFailed),
message: format!("webview_eval button textContent != 'Submit'; got {btn_text:?}"),
..Default::default()
}));
}
let result_text = app
.webview_eval(
"document.getElementById('user-input').value = 'hello'; \
document.getElementById('submit-form-btn').click(); \
document.getElementById('form-result').textContent",
)
.await?;
let result_str = result_text.as_str().unwrap_or("");
if result_str != "submitted: hello" {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::AssertionFailed),
message: format!(
"webview_eval mutate+click+read != 'submitted: hello'; got {result_text:?}"
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"v2_webview_basic",
n,
"SmixWebViewBridge /eval round-trip OK; DOM textContent read + mutate + click + read sequence verified",
),
Err(e) => harness.record_v2_e2e_fail(
"v2_webview_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_double_tap_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 home increment-btn — XCUI.doubleTap()",
"v2-home-increment-btn / v2-home-counter-label",
"v5.2 c3 SDK 自家路径 e2e — XCUIElement.doubleTap() public API path; \
expected counter increments by 2 (one tap per double-tap event).",
"v2_enter → tap v2-tab-home → reset counter → double_tap increment-btn → \
assert counter label > 0",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-home").await?;
let _ = app.tap_xcui("v2-home-reset-btn").await; tokio::time::sleep(Duration::from_millis(300)).await;
app.double_tap(&text("+1")).await?;
tokio::time::sleep(Duration::from_millis(400)).await;
let node = app
.find_one(&id("v2-home-counter-label"))
.await?
.ok_or_else(|| {
ExpectationFailure::new(FailureInit {
code: Some(FailureCode::ElementNotFound),
message: "v2-home-counter-label not found after double_tap".into(),
..Default::default()
})
})?;
let txt = node
.text
.as_deref()
.or(node.value.as_deref())
.or(node.label.as_deref())
.unwrap_or("");
if txt == "0" {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::AssertionFailed),
message: format!(
"double_tap did not increment counter; label still '0' (txt={txt:?})"
),
..Default::default()
}));
}
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"double_tap_basic",
n,
"XCUI.doubleTap() fired; counter advanced past 0",
),
Err(e) => harness.record_v2_e2e_fail(
"double_tap_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_long_press_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 home increment-btn — XCUI.press(forDuration:)",
"v2-home-increment-btn",
"v5.2 c3 SDK 自家路径 e2e — XCUIElement.press(forDuration:) public API. \
700ms hold; no specific side effect required, just that the call returns Ok \
(long-press semantics on a Button = same as tap unless explicit handler).",
"v2_enter → tap v2-tab-home → long_press increment-btn for 700ms → expect Ok",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-home").await?;
app.long_press(&text("+1"), Duration::from_millis(700))
.await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"long_press_basic",
n,
"XCUI.press(forDuration:) returned Ok; long-press gesture chain fired",
),
Err(e) => harness.record_v2_e2e_fail(
"long_press_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_copy_text_from_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"V2 home counter-label — App::copy_text_from(selector)",
"v2-home-counter-label / pasteboard",
"v5.2 c3 SDK 自家路径 e2e — copy_text_from reads element text (priority \
value→text→label) and writes to device pasteboard. Verify by get_clipboard \
returning non-empty text matching the digit pattern.",
"v2_enter → tap v2-tab-home → set_clipboard sentinel → copy_text_from \
v2-home-counter-label → get_clipboard expect non-sentinel value (counter digit)",
);
let result = async {
v2_enter(app, bundle).await?;
app.tap_xcui("v2-tab-home").await?;
app.set_clipboard("c-text-from-sentinel-PRE").await?;
app.copy_text_from(&id("v2-home-counter-label")).await?;
tokio::time::sleep(Duration::from_millis(200)).await;
let after = app.get_clipboard().await?;
if after == "c-text-from-sentinel-PRE" {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::AssertionFailed),
message: format!(
"copy_text_from did not overwrite clipboard; still sentinel {after:?}"
),
..Default::default()
}));
}
if after.is_empty() {
return Err(ExpectationFailure::new(FailureInit {
code: Some(FailureCode::AssertionFailed),
message: "copy_text_from wrote empty string to clipboard".into(),
..Default::default()
}));
}
Ok::<String, ExpectationFailure>(after)
}
.await;
match result {
Ok(text) => harness.record_v2_e2e_pass(
"copy_text_from_basic",
n,
&format!(
"counter-label text read + pasteboard write OK; clipboard = {:?}",
text
),
),
Err(e) => harness.record_v2_e2e_fail(
"copy_text_from_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
async fn seg_set_permissions_batch_basic(app: &App, harness: &mut Harness, bundle: &str) {
let n = Narration::new(
"(no UI — pure simctl wire probe)",
format!("bundle={bundle} permissions=[]"),
"v5.2 c5 SDK 自家路径 e2e — set_permissions is a batch wrapper around \
set_permission. Empty list = no-op probe (verifies the API surface returns \
Ok cleanly with zero entries, no panic).",
"app.set_permissions(bundle, &[]) → expect Ok",
);
let result = async {
app.set_permissions(bundle, &[]).await?;
Ok::<(), ExpectationFailure>(())
}
.await;
match result {
Ok(()) => harness.record_v2_e2e_pass(
"set_permissions_batch_basic",
n,
"set_permissions(bundle, &[]) returned Ok — batch wrapper API surface verified",
),
Err(e) => harness.record_v2_e2e_fail(
"set_permissions_batch_basic",
n,
&failure_kind_of(&e),
&e.message,
None,
),
}
}
fn failure_kind_of(e: &ExpectationFailure) -> String {
format!("{:?}", e.code)
}