use std::path::{Path, PathBuf};
use crate::tui::app::clickable::{self, ClickTarget};
struct Sandbox {
dir: PathBuf,
}
impl Sandbox {
fn new(tag: &str, files: &[&str]) -> Self {
let dir = std::env::temp_dir().join(format!("oc-click-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
for f in files {
let p = dir.join(f);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).expect("parent dirs");
}
std::fs::write(&p, b"fixture").expect("fixture write");
}
Self { dir }
}
fn path_str(&self, rel: &str) -> String {
self.dir.join(rel).display().to_string()
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn resolve(line: &str, col: usize, cwd: &Path, home: Option<&Path>) -> Option<ClickTarget> {
clickable::target_at_with(line, col, cwd, home)
}
fn mid_col(hay: &str, needle: &str) -> usize {
let byte = hay.find(needle).expect("needle present");
hay[..byte].chars().count() + needle.chars().count() / 2
}
#[test]
fn url_mid_sentence_resolves() {
let line = "See https://opencrabs.com/docs for more.";
let col = mid_col(line, "opencrabs");
let got = resolve(line, col, Path::new("/nonexistent-cwd"), None);
assert_eq!(
got,
Some(ClickTarget::Url("https://opencrabs.com/docs".into()))
);
}
#[test]
fn markdown_url_parenthesis_suffix_unwraps() {
let line = "truelens (https://truelens.tech) is it";
let col = mid_col(line, "truelens.tech");
let got = resolve(line, col, Path::new("/nonexistent-cwd"), None);
assert_eq!(got, Some(ClickTarget::Url("https://truelens.tech".into())));
}
#[test]
fn url_trailing_period_trimmed_but_balanced_inner_paren_kept() {
let line = "open https://x.dev/about. now";
let col = mid_col(line, "x.dev");
let got = resolve(line, col, Path::new("/nonexistent-cwd"), None);
assert_eq!(got, Some(ClickTarget::Url("https://x.dev/about".into())));
let wiki = "https://en.wikipedia.org/wiki/Foo_(bar)";
let col = mid_col(wiki, "wiki");
let got = resolve(wiki, col, Path::new("/nonexistent-cwd"), None);
assert_eq!(
got,
Some(ClickTarget::Url(
"https://en.wikipedia.org/wiki/Foo_(bar)".into()
))
);
}
#[test]
fn bare_scheme_does_not_launch() {
for line in ["https://", "http:// ...", "https://."] {
let col = 3;
assert_eq!(resolve(line, col, Path::new("/"), None), None, "{line}");
}
}
#[test]
fn existing_relative_path_resolves_against_cwd() {
let cwd = Sandbox::new("rel", &["docs/report.md"]);
let line = "written in docs/report.md, done";
let col = mid_col(line, "report.md");
let got = resolve(line, col, &cwd.dir, None);
assert_eq!(got, Some(ClickTarget::File(cwd.dir.join("docs/report.md"))));
}
#[test]
fn existing_absolute_path_resolves() {
let sb = Sandbox::new("abs", &["theme-background-audit.md"]);
let p = sb.path_str("theme-background-audit.md");
let line = format!("Audit with every file:line receipt: {p}");
let col = mid_col(&line, "audit.md");
let got = resolve(&line, col, Path::new("/"), None);
assert_eq!(got, Some(ClickTarget::File(PathBuf::from(&p))));
}
#[test]
fn trailing_period_on_path_is_trimmed() {
let sb = Sandbox::new("dot", &["note.md"]);
let p = sb.path_str("note.md");
let line = format!("see {p}. Done");
let col = mid_col(&line, "note.md");
let got = resolve(&line, col, Path::new("/"), None);
assert_eq!(got, Some(ClickTarget::File(PathBuf::from(&p))));
}
#[test]
fn tilde_expands_only_with_home() {
let home = Sandbox::new("home", &["research/note.md"]);
let line = "read ~/research/note.md now";
let col = mid_col(line, "note.md");
let got = resolve(line, col, Path::new("/"), Some(&home.dir));
assert_eq!(
got,
Some(ClickTarget::File(home.dir.join("research/note.md")))
);
assert_eq!(resolve(line, col, Path::new("/"), None), None);
}
#[test]
fn file_line_reference_opens_the_file() {
let cwd = Sandbox::new("lineref", &["chat.rs"]);
let line = "receipt chat.rs:1618 proves it";
let col = mid_col(line, ":1618");
let got = resolve(line, col, &cwd.dir, None);
assert_eq!(got, Some(ClickTarget::File(cwd.dir.join("chat.rs"))));
}
#[test]
fn nonexistent_path_is_inert() {
let line = "legacy notes in /definitely/not/here_42.md, old";
let col = mid_col(line, "here_42.md");
assert_eq!(resolve(line, col, Path::new("/"), None), None);
}
#[test]
fn prose_words_are_inert() {
for (line, needle) in [
("the audit is done.", "audit"),
("e.g. run it", "e.g"),
("Meeting at 14:30 soon", "14:30"),
("plain", "plain"),
] {
let col = mid_col(line, needle);
assert_eq!(
resolve(line, col, Path::new("/"), None),
None,
"must not launch: {line:?}"
);
}
}
#[test]
fn click_on_whitespace_or_past_eol_is_inert() {
let line = "x https://a.dev";
assert_eq!(resolve(line, 1, Path::new("/"), None), None); assert_eq!(resolve(line, line.len() + 4, Path::new("/"), None), None);
}
#[test]
fn unicode_columns_still_hit() {
let line = "日本語See https://x.dev/a now";
let col = mid_col(line, "x.dev");
let got = resolve(line, col, Path::new("/"), None);
assert_eq!(got, Some(ClickTarget::Url("https://x.dev/a".into())));
}
#[test]
fn backtick_wrapped_path_resolves() {
let sb = Sandbox::new("btick", &["README.md"]);
let p = sb.path_str("README.md");
let line = format!("run `{p}` now");
let col = mid_col(&line, "README.md");
let got = resolve(&line, col, Path::new("/"), None);
assert_eq!(got, Some(ClickTarget::File(PathBuf::from(&p))));
}
#[test]
fn file_scheme_and_mailto() {
let sb = Sandbox::new("fscheme", &["a.md"]);
let p = sb.path_str("a.md");
let line = format!("open file://{p} here");
let col = mid_col(&line, "a.md");
let got = resolve(&line, col, Path::new("/"), None);
assert_eq!(got, Some(ClickTarget::File(PathBuf::from(&p))));
let line = "mail mailto:ada@example.com";
let col = mid_col(line, "ada@example.com");
let got = resolve(line, col, Path::new("/"), None);
assert_eq!(got, Some(ClickTarget::Url("mailto:ada@example.com".into())));
let line = "mailto: is a scheme";
let col = mid_col(line, "mailto:");
assert_eq!(resolve(line, col, Path::new("/"), None), None);
}
#[test]
fn mouse_up_resolves_before_click_select() {
let src = include_str!("../tui/app/input.rs");
let up = src
.find("pub(crate) fn handle_mouse_up")
.expect("handle_mouse_up exists");
let body_end = src[up..]
.find("\n /// ")
.map(|e| up + e)
.unwrap_or(src.len());
let body = &src[up..body_end];
let resolve_at = body
.find("try_open_clicked(col, row)")
.expect("handle_mouse_up calls try_open_clicked");
let select_at = body
.find("self.handle_click_select(row)")
.expect("handle_mouse_up still falls back to click-select");
assert!(
resolve_at < select_at,
"resolver must run before the click-select fallback"
);
assert!(
src.contains("super::clickable::target_at"),
"try_open_clicked must go through the clickable resolver"
);
}
#[test]
fn clickable_module_registered_and_launcher_detached() {
let mods = include_str!("../tui/app/mod.rs");
assert!(
mods.contains("pub(crate) mod clickable;"),
"module registered"
);
let src = include_str!("../tui/app/clickable.rs");
assert!(src.contains(".spawn()"), "launcher uses spawn, not status");
}
#[test]
fn opener_command_matches_platform() {
let cmd = clickable::opener_command();
#[cfg(target_os = "macos")]
let expected = "open";
#[cfg(target_os = "linux")]
let expected = "xdg-open";
#[cfg(target_os = "windows")]
let expected = "explorer";
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let expected = "xdg-open";
assert_eq!(cmd, expected);
assert!(
matches!(cmd, "open" | "xdg-open" | "explorer"),
"unexpected launcher: {cmd}"
);
}
#[test]
fn docs_advertise_click_to_open() {
let readme = include_str!("../../README.md");
let row = readme
.lines()
.find(|l| l.contains("`Mouse click`"))
.expect("README documents Mouse click row");
assert!(
row.contains("open") && row.contains("URL"),
"README row must describe opening URLs/paths: {row}"
);
let help = include_str!("../tui/render/help.rs");
assert!(
help.contains("kv(\"Mouse click\""),
"/help dialog must list the Mouse click binding"
);
}