You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/27 00:13:17 UTC
(maka) branch feat/runtime-host-rust updated: fix(computer-use): retain native bindings across transient AX failures
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
new fce8d3490e fix(computer-use): retain native bindings across transient AX failures
fce8d3490e is described below
commit fce8d3490e634500477c1f147155c8d7874eb6b4
Author: Wang <m4...@gmail.com>
AuthorDate: Sun Sep 27 08:13:06 2026 +0800
fix(computer-use): retain native bindings across transient AX failures
Invalidate observations when accessibility reads fail while preserving the retained target identity for a later refresh. Continue rejecting closed windows, stale indices and stale screenshot coordinates. Distinguish missing text from ambiguous selection matches.
Exercise app busy/resume, external edits and window closure through a background AppKit fixture without keyboard input. Document the foreground requirements of the separate rich paste acceptance test.
Generated-by: Codex
---
crates/computer-use/README.md | 6 +
crates/computer-use/README.zh-CN.md | 3 +
crates/computer-use/src/api.md | 4 +
crates/computer-use/src/browser/select-text.js | 7 +-
crates/computer-use/src/macos.rs | 26 ++-
crates/computer-use/src/session.rs | 72 +++---
.../computer-use/tests/fixtures/native-form.swift | 37 ++-
crates/computer-use/tests/repl_browser.rs | 259 +++++++++++++++++----
8 files changed, 329 insertions(+), 85 deletions(-)
diff --git a/crates/computer-use/README.md b/crates/computer-use/README.md
index 7bffbc614a..f20c981e55 100644
--- a/crates/computer-use/README.md
+++ b/crates/computer-use/README.md
@@ -124,5 +124,11 @@ in `tests/repl_browser.rs` operate disposable fixtures: a temporary Chrome profi
and a compiled AppKit form, including Unicode editing, rich paste, screenshots,
navigation and stale targets. Set `MAKA_CUA_TEST_CHROME` to override the Chrome binary.
They are separate from ordinary CI tests and do not prove untested desktop behavior.
+The form editing test temporarily takes keyboard focus; run it while the user is not
+typing. The recovery test stays in the background and uses no keyboard input:
+
+```sh
+cargo nextest run -p maka-computer-use --test repl_browser -E 'test(native_observation_recovers)' --run-ignored all
+```
See [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES) for source and licensing.
diff --git a/crates/computer-use/README.zh-CN.md b/crates/computer-use/README.zh-CN.md
index ad25c73c32..dfada86cea 100644
--- a/crates/computer-use/README.zh-CN.md
+++ b/crates/computer-use/README.zh-CN.md
@@ -52,4 +52,7 @@ extension/IAB 提供端、visibility/sessionName、交付 UI 标记或 CDP 辅
测试包括独立 REPL/权限/取消/资源边界,以及需主动运行的真实 Chrome、AppKit 表单
验收。验证成功的范围与完整 cua_repl 等价、性能更优是不同结论,后两者尚未宣称。
+表单编辑验收会短暂切换键盘焦点,需要在用户没有打字时运行;
+`native_observation_recovers` 验收始终在后台运行,不发送键盘输入,并检查应用忙后恢复、
+旧观察失效、外部编辑保留和窗口关闭后的拒绝。
构建、配置、测试边界与许可说明见 [English README](README.md)。
diff --git a/crates/computer-use/src/api.md b/crates/computer-use/src/api.md
index f44bfca804..757152a31b 100644
--- a/crates/computer-use/src/api.md
+++ b/crates/computer-use/src/api.md
@@ -20,6 +20,8 @@ App and Tab observations:
- `getAXStateAndScreenshot(options?)` returns `{state, screenshot}` and emits both.
Observations emit by default; use `{emit:false}` when processing the result yourself.
Screenshot-only observation clears accessibility indices.
+If a native AX observation fails while an app is busy, refresh the same bound object
+when the app responds again. Failed observations invalidate old indices and coordinates.
App and Tab actions:
- `click(index | [x,y], {mouseButton?, clickCount?})`, `drag([x,y], [x,y])`.
@@ -27,6 +29,8 @@ App and Tab actions:
- `setValue(index, string)`.
- `selectText(index, text, {prefix?, suffix?, selectionType?})` where selectionType is
`text`, `cursor_before` or `cursor_after`. Native selection is macOS-only.
+ `text_not_found` means the text or supplied context is absent; read the current
+ state again. `ambiguous_text` requires a more specific prefix/suffix.
- `performSecondaryAction(index, action)`: use an exact action listed in the native
AX observation. CDP does not expose secondary actions and rejects this method.
diff --git a/crates/computer-use/src/browser/select-text.js b/crates/computer-use/src/browser/select-text.js
index c1fdee0e98..d7e539d4be 100644
--- a/crates/computer-use/src/browser/select-text.js
+++ b/crates/computer-use/src/browser/select-text.js
@@ -34,8 +34,11 @@ function selectText(text, prefix, suffix, selectionType) {
matches.push(at);
from = at + 1;
}
- if (matches.length !== 1)
- throw Error('text match is missing or ambiguous; provide prefix/suffix');
+ if (matches.length === 0)
+ throw Error(
+ 'text_not_found: no match for text and prefix/suffix; read getAXState again because the content may have changed',
+ );
+ if (matches.length > 1) throw Error('ambiguous_text: multiple matches; provide prefix/suffix');
let start = matches[0],
end = start + text.length;
if (selectionType === 'cursor_before') end = start;
diff --git a/crates/computer-use/src/macos.rs b/crates/computer-use/src/macos.rs
index aa9074731b..802e087a71 100644
--- a/crates/computer-use/src/macos.rs
+++ b/crates/computer-use/src/macos.rs
@@ -169,7 +169,9 @@ impl Target {
}
CFRelease(window.cast());
}
- let window = selected.ok_or_else(|| failed("AX window unavailable"))?;
+ let window = selected.ok_or_else(|| {
+ failed("AX window unavailable; the application may be busy or the window closed. Read getAXState again when it is responsive")
+ })?;
Ok(Self {
pid,
window_id,
@@ -474,9 +476,14 @@ fn selection(
})
.map(|(at, _)| at)
.collect();
- if matches.len() != 1 {
+ if matches.is_empty() {
return Err(failed(
- "text match is missing or ambiguous; provide prefix/suffix",
+ "text_not_found: no match for text and prefix/suffix; read getAXState again because the content may have changed",
+ ));
+ }
+ if matches.len() > 1 {
+ return Err(failed(
+ "ambiguous_text: multiple matches; provide prefix/suffix",
));
}
let start = value[..matches[0]].encode_utf16().count();
@@ -512,7 +519,18 @@ mod tests {
selection("😀 hello hello!", "hello", &options).unwrap(),
(9, 5)
);
- assert!(selection("aaa", "aa", &SelectOptions::default()).is_err());
+ assert!(
+ selection("aaa", "aa", &SelectOptions::default())
+ .unwrap_err()
+ .to_string()
+ .contains("ambiguous_text")
+ );
+ assert!(
+ selection("edited", "hello", &SelectOptions::default())
+ .unwrap_err()
+ .to_string()
+ .contains("text_not_found")
+ );
assert!(selection("abc", "", &SelectOptions::default()).is_err());
}
}
diff --git a/crates/computer-use/src/session.rs b/crates/computer-use/src/session.rs
index 56e6f05b66..ee55af8aee 100644
--- a/crates/computer-use/src/session.rs
+++ b/crates/computer-use/src/session.rs
@@ -228,14 +228,21 @@ impl Session {
kind,
options,
} => {
- self.observe(
- &handle,
- kind,
- !options.disable_diffing,
- session,
- cancellation,
- )
- .await
+ let result = self
+ .observe(
+ &handle,
+ kind,
+ !options.disable_diffing,
+ session,
+ cancellation,
+ )
+ .await;
+ if result.is_err()
+ && let Some(target) = self.targets.get_mut(&handle)
+ {
+ target.invalidate_observation();
+ }
+ result
}
Command::Action {
handle: Handle::App(handle),
@@ -300,7 +307,17 @@ impl Session {
}
let id = target.window.window_id;
#[cfg(target_os = "macos")]
- let geometry = if let Some(macos) = target.macos.clone() {
+ let macos = target.macos.clone();
+ let windows = self.windows(session, cancellation).await?;
+ let Some(window) = windows
+ .into_iter()
+ .find(|w| w.pid == Some(pid) && w.window_id == id)
+ else {
+ self.targets.remove(handle);
+ return Err(failed("stale_target: window closed; bind a current window"));
+ };
+ #[cfg(target_os = "macos")]
+ let window = if let Some(macos) = macos {
let verified = tokio::task::spawn_blocking(move || {
let target = macos.lock().unwrap();
target.verify()?;
@@ -309,30 +326,22 @@ impl Session {
.await
.map_err(failed)?;
match verified {
- Ok(bounds) => Some(bounds),
+ Ok(bounds) => WindowInfo { bounds, ..window },
Err(error) => {
- self.targets.remove(handle);
+ // AX timeouts do not prove that the window was replaced.
+ // Keep its retained identity, but require a fresh observation
+ // before any indexed or coordinate input can resume.
+ self.targets
+ .get_mut(handle)
+ .unwrap()
+ .invalidate_observation();
return Err(error);
}
}
} else {
- None
+ window
};
- let windows = self.windows(session, cancellation).await?;
- if let Some(window) = windows
- .into_iter()
- .find(|w| w.pid == Some(pid) && w.window_id == id)
- {
- #[cfg(target_os = "macos")]
- let window = if let Some(bounds) = geometry {
- WindowInfo { bounds, ..window }
- } else {
- window
- };
- return Ok(window);
- }
- self.targets.remove(handle);
- Err(failed("stale_target: window closed; bind a current window"))
+ Ok(window)
}
async fn observe(
&mut self,
@@ -642,6 +651,15 @@ impl Session {
}
}
impl Target {
+ fn invalidate_observation(&mut self) {
+ self.snapshot = None;
+ self.capture = None;
+ self.previous = None;
+ #[cfg(target_os = "macos")]
+ if let Some(macos) = &self.macos {
+ macos.lock().unwrap().clear();
+ }
+ }
fn element(&self, index: u64) -> Result<&cua_driver_contract::WindowElement, ToolError> {
self.snapshot
.as_ref()
diff --git a/crates/computer-use/tests/fixtures/native-form.swift b/crates/computer-use/tests/fixtures/native-form.swift
index 9a51ee0183..1d83d9ac37 100644
--- a/crates/computer-use/tests/fixtures/native-form.swift
+++ b/crates/computer-use/tests/fixtures/native-form.swift
@@ -24,6 +24,7 @@ final class Fixture: NSObject, NSApplicationDelegate {
let name = NSTextField(string: "hello hello")
let message = NSTextView()
let output = NSTextField(labelWithString: "Not submitted")
+ let resume = DispatchSemaphore(value: 0)
func applicationDidFinishLaunching(_ notification: Notification) {
let menu = NSMenu()
@@ -39,6 +40,7 @@ final class Fixture: NSObject, NSApplicationDelegate {
window = NSWindow(contentRect: NSRect(x: 220, y: 220, width: 420, height: 250), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false)
window.title = "Maka Cua Form Fixture"
+ window.isReleasedWhenClosed = false
name.frame = NSRect(x: 24, y: 190, width: 360, height: 28)
name.setAccessibilityLabel("Name")
message.frame = NSRect(x: 24, y: 140, width: 360, height: 28)
@@ -47,11 +49,40 @@ final class Fixture: NSObject, NSApplicationDelegate {
button.frame = NSRect(x: 24, y: 85, width: 160, height: 32)
output.frame = NSRect(x: 24, y: 25, width: 370, height: 40)
for view in [name, message, button, output] { window.contentView!.addSubview(view) }
- window.makeKeyAndOrderFront(nil)
- NSApp.activate(ignoringOtherApps: true)
+ if ProcessInfo.processInfo.arguments.contains("--background") {
+ window.orderBack(nil)
+ } else {
+ window.makeKeyAndOrderFront(nil)
+ NSApp.activate(ignoringOtherApps: true)
+ }
FileHandle.standardOutput.write(Data("ready \(window.windowNumber)\n".utf8))
DispatchQueue.global().async { [self] in
while let command = readLine() {
+ if command == "activity" {
+ DispatchQueue.main.async {
+ FileHandle.standardOutput.write(Data((NSApp.isActive ? "active\n" : "inactive\n").utf8))
+ }
+ }
+ if command == "pause" {
+ DispatchQueue.main.async { [self] in
+ FileHandle.standardOutput.write(Data("paused\n".utf8))
+ resume.wait()
+ FileHandle.standardOutput.write(Data("resumed\n".utf8))
+ }
+ }
+ if command == "resume" { resume.signal() }
+ if command == "edit" {
+ DispatchQueue.main.async { [self] in
+ name.stringValue = "externally edited"
+ FileHandle.standardOutput.write(Data("edited\n".utf8))
+ }
+ }
+ if command == "close" {
+ DispatchQueue.main.async { [self] in
+ window.close()
+ FileHandle.standardOutput.write(Data("closed\n".utf8))
+ }
+ }
if command == "move" {
DispatchQueue.main.async { [self] in
let before = window.frame.origin
@@ -73,7 +104,7 @@ final class Fixture: NSObject, NSApplicationDelegate {
}
}
let app = NSApplication.shared
-app.setActivationPolicy(.regular)
+app.setActivationPolicy(ProcessInfo.processInfo.arguments.contains("--background") ? .accessory : .regular)
let fixture = Fixture()
app.delegate = fixture
app.run()
diff --git a/crates/computer-use/tests/repl_browser.rs b/crates/computer-use/tests/repl_browser.rs
index a90dc72abd..74cbfef8e8 100644
--- a/crates/computer-use/tests/repl_browser.rs
+++ b/crates/computer-use/tests/repl_browser.rs
@@ -94,40 +94,8 @@ fn index(state: &str, role: &str, name: &str) -> u64 {
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
#[ignore = "requires macOS Accessibility; compiles and operates a disposable native form"]
async fn native_form_retains_exact_ax_elements_and_verifies_submission() {
- assert!(
- unsafe { platform_macos::ax::bindings::AXIsProcessTrusted() },
- "native acceptance requires macOS Accessibility permission for the test host"
- );
- let directory = tempfile::tempdir().unwrap();
- let contents = directory.path().join("Maka Cua Fixture.app/Contents");
- std::fs::create_dir_all(contents.join("MacOS")).unwrap();
- std::fs::write(contents.join("Info.plist"),r#"<?xml version="1.0" encoding="UTF-8"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>org.apache.maka.cua.fixture.FIXTURE_ID</string><key>CFBundleName</key><string>Maka Cua Fixture</string><key>CFBundleExecutable</key><string>fixture</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>"#.replace("FIXTURE_ID", &uuid::Uuid::new_v4().to_string())).unwrap();
- let executable = contents.join("MacOS/fixture");
- let compiled = tokio::process::Command::new("/usr/bin/swiftc")
- .arg(concat!(
- env!("CARGO_MANIFEST_DIR"),
- "/tests/fixtures/native-form.swift"
- ))
- .arg("-o")
- .arg(&executable)
- .output()
- .await
- .unwrap();
- assert!(
- compiled.status.success(),
- "{}",
- String::from_utf8_lossy(&compiled.stderr)
- );
- let mut app = tokio::process::Command::new(&executable)
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::null())
- .kill_on_drop(true)
- .spawn()
- .unwrap();
- let mut lines = BufReader::new(app.stdout.take().unwrap()).lines();
- let ready = lines.next_line().await.unwrap().unwrap();
- let window_id: u64 = ready.strip_prefix("ready ").unwrap().parse().unwrap();
+ let mut form = NativeForm::start(false).await;
+ let window_id = form.window_id;
let driver = Arc::new(Mutex::new(Driver::default()));
let mut native = Session::new(driver.clone());
let windows = native
@@ -167,11 +135,7 @@ async fn native_form_retains_exact_ax_elements_and_verifies_submission() {
let button = index(&state, "AXButton", "Submit fixture");
let (ok,output)=evaluate(&repl,bridge.clone(),format!("await app.click({name}); await app.selectText({name}, 'hello', {{prefix:'hello '}}); await app.paste('world'); await app.setValue({message}, 'placeholder'); await app.click({message}); await app.selectText({message}, 'placeholder'); await app.paste('<b>你好</b>', {{format:'html'}}); await app.click({button}); await app.getAXState({{disableDiffing:true}});")).await;
assert!(ok, "{}", texts(&output));
- let submitted = tokio::time::timeout(Duration::from_secs(5), lines.next_line())
- .await
- .unwrap()
- .unwrap()
- .unwrap();
+ let submitted = form.line().await;
let submitted: Value = serde_json::from_str(&submitted).unwrap();
assert!(
submitted["submitted"] == "hello world / 你好",
@@ -201,20 +165,13 @@ async fn native_form_retains_exact_ax_elements_and_verifies_submission() {
.iter()
.any(|part| matches!(part, CellOutput::Media { .. }))
);
- app.stdin
- .as_mut()
- .unwrap()
- .write_all(b"move\n")
- .await
- .unwrap();
- assert_eq!(lines.next_line().await.unwrap().as_deref(), Some("moved"));
+ assert_eq!(form.command("move").await, "moved");
let (ok, _) = evaluate(&repl, bridge.clone(), "await app.click([5,5]);".into()).await;
assert!(
!ok,
"external window movement must reject old screenshot coordinates"
);
- app.kill().await.unwrap();
- app.wait().await.unwrap();
+ form.stop().await;
let (ok, _) = evaluate(
&repl,
bridge.clone(),
@@ -227,6 +184,191 @@ async fn native_form_retains_exact_ax_elements_and_verifies_submission() {
driver.lock().await.close().await.unwrap();
}
+#[cfg(target_os = "macos")]
+#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
+#[ignore = "requires macOS Accessibility; observes a background fixture without keyboard input"]
+async fn native_observation_recovers_after_busy_and_preserves_external_edits() {
+ let mut form = NativeForm::start(true).await;
+ assert_eq!(form.command("activity").await, "inactive");
+ let driver = Arc::new(Mutex::new(Driver::default()));
+ let bridge = Arc::new(Bridge(Arc::new(Mutex::new(Session::new(driver.clone())))));
+ let repl = Repl::new(
+ facade(),
+ CellLimits {
+ max_value_bytes: 16 * 1024 * 1024,
+ heap_bytes: 128 * 1024 * 1024,
+ ..Default::default()
+ },
+ );
+ let (ok, output) = evaluate(
+ &repl,
+ bridge.clone(),
+ format!(
+ "const app = await cua.getApp({{windowId:{}}}); await app.getAXStateAndScreenshot();",
+ form.window_id
+ ),
+ )
+ .await;
+ assert!(ok, "{}", texts(&output));
+ let name = index(&texts(&output), "AXTextField", "Name");
+
+ // Hold the fixture's UI thread until the AX request times out, then
+ // explicitly release it. No timing assumption or physical input is needed.
+ assert_eq!(form.command("pause").await, "paused");
+ let (ok, output) = evaluate(
+ &repl,
+ bridge.clone(),
+ "try { await app.getAXState(); } catch (e) { nodeRepl.write(String(e)); }".into(),
+ )
+ .await;
+ assert!(
+ ok && texts(&output).contains("AX window unavailable"),
+ "{}",
+ texts(&output)
+ );
+ assert_eq!(form.command("resume").await, "resumed");
+
+ let (ok, output) = evaluate(&repl, bridge.clone(), format!(
+ "try {{ await app.setValue({name}, 'must not overwrite'); }} catch (e) {{ nodeRepl.write(String(e)); }}
+ try {{ await app.click([5,5]); }} catch (e) {{ nodeRepl.write(String(e)); }}"
+ )).await;
+ assert!(
+ ok && texts(&output).contains("stale_element")
+ && texts(&output).contains("capture_required"),
+ "{}",
+ texts(&output)
+ );
+ let (ok, output) = evaluate(&repl, bridge.clone(), "await app.getAXState();".into()).await;
+ assert!(
+ ok && texts(&output).contains("hello hello"),
+ "the same binding must recover without rebind: {}",
+ texts(&output)
+ );
+ let name = index(&texts(&output), "AXTextField", "Name");
+
+ assert_eq!(form.command("edit").await, "edited");
+ let (ok, output) = evaluate(&repl, bridge.clone(), format!(
+ "try {{ await app.selectText({name}, 'hello', {{prefix:'hello '}}); }} catch (e) {{ nodeRepl.write(String(e)); }}"
+ )).await;
+ assert!(
+ ok && texts(&output).contains("text_not_found"),
+ "{}",
+ texts(&output)
+ );
+ let (ok, output) = evaluate(&repl, bridge.clone(), "await app.getAXState();".into()).await;
+ assert!(
+ ok && texts(&output).contains("externally edited"),
+ "external edits must remain intact: {}",
+ texts(&output)
+ );
+
+ assert_eq!(form.command("activity").await, "inactive");
+ assert_eq!(form.command("close").await, "closed");
+ assert!(
+ form.app.try_wait().unwrap().is_none(),
+ "the window closed, not the process"
+ );
+ let (ok, _) = evaluate(
+ &repl,
+ bridge.clone(),
+ format!("await app.setValue({name}, 'closed window');"),
+ )
+ .await;
+ assert!(
+ !ok,
+ "preserving a binding must not permit input to a closed window"
+ );
+ form.stop().await;
+ repl.close().await.unwrap();
+ bridge.0.lock().await.close().await.unwrap();
+ driver.lock().await.close().await.unwrap();
+}
+
+#[cfg(target_os = "macos")]
+struct NativeForm {
+ app: tokio::process::Child,
+ lines: tokio::io::Lines<BufReader<tokio::process::ChildStdout>>,
+ window_id: u64,
+ _directory: tempfile::TempDir,
+}
+
+#[cfg(target_os = "macos")]
+impl NativeForm {
+ async fn start(background: bool) -> Self {
+ assert!(
+ unsafe { platform_macos::ax::bindings::AXIsProcessTrusted() },
+ "native acceptance requires macOS Accessibility permission for the test host"
+ );
+ let directory = tempfile::tempdir().unwrap();
+ let contents = directory.path().join("Maka Cua Fixture.app/Contents");
+ std::fs::create_dir_all(contents.join("MacOS")).unwrap();
+ std::fs::write(contents.join("Info.plist"),r#"<?xml version="1.0" encoding="UTF-8"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>org.apache.maka.cua.fixture.FIXTURE_ID</string><key>CFBundleName</key><string>Maka Cua Fixture</string><key>CFBundleExecutable</key><string>fixture</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>"#.replace("FIXTURE_ID", &uuid::Uuid::new_v4().to_string())).unwrap();
+ let executable = contents.join("MacOS/fixture");
+ let compiled = tokio::process::Command::new("/usr/bin/swiftc")
+ .arg(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/tests/fixtures/native-form.swift"
+ ))
+ .arg("-o")
+ .arg(&executable)
+ .output()
+ .await
+ .unwrap();
+ assert!(
+ compiled.status.success(),
+ "{}",
+ String::from_utf8_lossy(&compiled.stderr)
+ );
+ let mut launch = tokio::process::Command::new(&executable);
+ if background {
+ launch.arg("--background");
+ }
+ let mut app = launch
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .kill_on_drop(true)
+ .spawn()
+ .unwrap();
+ let lines = BufReader::new(app.stdout.take().unwrap()).lines();
+ let mut fixture = Self {
+ app,
+ lines,
+ window_id: 0,
+ _directory: directory,
+ };
+ fixture.window_id = fixture
+ .line()
+ .await
+ .strip_prefix("ready ")
+ .unwrap()
+ .parse()
+ .unwrap();
+ fixture
+ }
+ async fn line(&mut self) -> String {
+ tokio::time::timeout(Duration::from_secs(5), self.lines.next_line())
+ .await
+ .expect("native fixture responds")
+ .unwrap()
+ .expect("native fixture remains alive")
+ }
+ async fn command(&mut self, command: &str) -> String {
+ self.app
+ .stdin
+ .as_mut()
+ .unwrap()
+ .write_all(format!("{command}\n").as_bytes())
+ .await
+ .unwrap();
+ self.line().await
+ }
+ async fn stop(&mut self) {
+ self.app.kill().await.unwrap();
+ self.app.wait().await.unwrap();
+ }
+}
+
#[cfg(target_os = "macos")]
fn clipboard_snapshot() -> Vec<Vec<(String, Vec<u8>)>> {
objc2_app_kit::NSPasteboard::generalPasteboard()
@@ -328,7 +470,26 @@ async fn browser_form_uses_independent_repl_and_rejects_stale_document_and_tab()
)
.await;
assert!(ok, "{}", texts(&output));
- let state = texts(&output);
+ let mut state = texts(&output);
+ for (text, diagnostic) in [("missing", "text_not_found"), ("hello", "ambiguous_text")] {
+ let name = index(&state, "textbox", "Name ");
+ let (ok, output) = evaluate(&repl, bridge.clone(), format!(
+ "try {{ await tab.selectText({name}, {text:?}); }} catch (e) {{ nodeRepl.write(String(e)); }}"
+ )).await;
+ assert!(
+ ok && texts(&output).contains(diagnostic),
+ "{}",
+ texts(&output)
+ );
+ let (ok, output) = evaluate(
+ &repl,
+ bridge.clone(),
+ "await tab.getAXState({disableDiffing:true});".into(),
+ )
+ .await;
+ assert!(ok);
+ state = texts(&output);
+ }
let name = index(&state, "textbox", "Name ");
let message = index(&state, "textbox", "Message ");
let color = index(&state, "combobox", "Color");
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org