Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions crates/ff-rdp-cli/src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ COMMAND REFERENCE:

Accessibility:
ff-rdp a11y [--depth N] [--selector SEL] [--interactive] [--critical]
ff-rdp a11y --native # opt in to Firefox's platform accessibility tree (meta.source tells you which tree you got)
ff-rdp a11y contrast [--selector SEL] [--fail-only] # total=results, sampled=elements checked
ff-rdp a11y summary

Expand Down Expand Up @@ -153,6 +154,8 @@ COOKBOOK:
ff-rdp a11y contrast --fail-only # total = AA failures; sampled = elements checked
ff-rdp a11y contrast --fail-only --jq '{total, shown: (.results|length), sampled}'
ff-rdp a11y --interactive --jq '[.. | select(.role? == \"link\") | .name]'
ff-rdp a11y --jq '.meta.source' # \"native\" or \"js-fallback\" — always present
ff-rdp a11y --native --jq '.result.role' # opt in to the real platform tree ([\"document\", ...])

# DOM and CSS inspection
ff-rdp dom \"a[href]\" --text-attrs
Expand Down Expand Up @@ -788,10 +791,11 @@ With --key: {\"results\": {\"key\": \"...\", \"value\": \"...\"}, \"total\": 1,
/// Inspect accessibility tree and check WCAG compliance
#[command(long_about = "Inspect accessibility tree and check WCAG compliance.

Output: {\"results\": {\"role\": \"...\", \"name\": \"...\", \"children\": [...]}, \"total\": 1, \"meta\": {...}}
Output: {\"results\": {\"role\": \"...\", \"name\": \"...\", \"children\": [...]}, \"total\": 1, \"meta\": {..., \"source\": \"native\"|\"js-fallback\"}}
With a11y summary: {\"results\": [{\"role\": \"...\", \"name\": \"...\", \"level\": N}], \"total\": N, \"meta\": {...}}
With a11y contrast: {\"results\": [{\"selector\": \"...\", \"ratio\": N, \"aa_normal\": bool, ...}], \"total\": N, \"sampled\": M, \"meta\": {...}}
a11y contrast `total` = returned results (AA failures under --fail-only, else all checks); `sampled` = elements examined. Pre-iter-127 `total` reported the sample size.")]
With a11y contrast: {\"results\": [{\"selector\": \"...\", \"ratio\": N, \"aa_normal\": bool, ...}], \"total\": N, \"sampled\": M, \"meta\": {..., \"source\": \"js-fallback\"}}
a11y contrast `total` = returned results (AA failures under --fail-only, else all checks); `sampled` = elements examined. Pre-iter-127 `total` reported the sample size.
`meta.source` (iter-143) is always present on a11y/a11y --critical/a11y contrast: \"native\" means the real Firefox platform accessibility tree (roles like \"document\"/\"paragraph\"); \"js-fallback\" means a DOM-derived approximation (roles like \"generic\"), with `meta.source_reason` naming why. `a11y --native` opts in to the native tree (never the default — DEC-027) by enabling Firefox's accessibility service for the duration of the call.")]
A11y(A11yArgs),
/// Reload the page
#[command(long_about = "Reload the page.
Expand Down Expand Up @@ -1794,6 +1798,17 @@ pub struct A11yArgs {
/// instead of the full accessibility tree; empty when nothing critical.
#[arg(long, conflicts_with = "interactive")]
pub critical: bool,
/// Opt in to the native platform accessibility tree: enables Firefox's
/// accessibility service (if not already running), walks the real
/// platform tree (roles like "document"/"paragraph" instead of the
/// JS-derived "generic"), then restores the service to its previous
/// state if this command was the one that turned it on. This is a
/// browser-global, process-wide change while it runs — never the
/// default (DEC-027) — and cannot be combined with `--selector`/`--ref`,
/// which always use the JS-derived path. Failure to enable surfaces as
/// an explicit error, never a silent fallback.
#[arg(long, conflicts_with_all = ["selector", "ref_id"])]
pub native: bool,
}

#[derive(clap::Args)]
Expand Down
273 changes: 258 additions & 15 deletions crates/ff-rdp-cli/src/commands/a11y.rs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions crates/ff-rdp-cli/src/commands/a11y_contrast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ pub fn run(cli: &Cli, selector: Option<&str>, fail_only: bool) -> Result<(), App
// agent can tell how this command executed without a
// separate `daemon status` round-trip.
crate::connection_meta::merge_route(&mut meta, ctx.via_daemon);
// iter-143 Theme A: contrast checking is always DOM/computed-style based
// — there is no native-actor equivalent — so this is always
// "js-fallback". Reported for consistency with `a11y`'s `meta.source`.
crate::connection_meta::merge_source(&mut meta, "js-fallback", Some("contrast-audit-js-only"));

// Apply output controls (sort, limit, fields).
let controls = OutputControls::from_cli(cli, SortDir::Desc);
Expand Down
48 changes: 48 additions & 0 deletions crates/ff-rdp-cli/src/connection_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ pub fn merge_route(meta: &mut Value, via_daemon: bool) {
}
}

/// Merge `meta.source` — `"native"` or `"js-fallback"` — into `meta`
/// (iter-143 Theme A).
///
/// Same unconditional treatment as [`merge_route`]: a caller scoring
/// accessibility output needs to know which tree it is looking at — the
/// native platform tree (real accessible roles like `document`/`paragraph`)
/// or the DOM-derived approximation (`generic`, …) — without a separate
/// `--verbose` round-trip. `reason` is populated only when `source` is
/// `"js-fallback"`, naming why the native path was not used (e.g.
/// `"accessibility-service-disabled"`, `"selector-mode"`).
pub fn merge_source(meta: &mut Value, source: &str, reason: Option<&str>) {
if let Some(obj) = meta.as_object_mut() {
obj.insert("source".to_string(), Value::String(source.to_owned()));
if let Some(r) = reason {
obj.insert("source_reason".to_string(), Value::String(r.to_owned()));
}
}
}

fn cached_owner(host: &str, port: u16) -> Option<PortOwner> {
// Only cache for loopback hosts. A remote port would require a different
// lookup strategy entirely; we just skip the cache for those.
Expand Down Expand Up @@ -240,6 +259,35 @@ mod tests {
assert_eq!(meta["route"], "direct");
}

/// iter-143 Theme A: `merge_source` sets `source` unconditionally and
/// omits `source_reason` when none is given (the native path).
#[test]
fn merge_source_native_omits_reason() {
let mut meta = json!({});
merge_source(&mut meta, "native", None);
assert_eq!(meta["source"], "native");
assert!(meta.get("source_reason").is_none());
}

#[test]
fn merge_source_js_fallback_includes_reason() {
let mut meta = json!({});
merge_source(
&mut meta,
"js-fallback",
Some("accessibility-service-disabled"),
);
assert_eq!(meta["source"], "js-fallback");
assert_eq!(meta["source_reason"], "accessibility-service-disabled");
}

#[test]
fn merge_source_makes_meta_non_empty() {
let mut meta = json!({});
merge_source(&mut meta, "native", None);
assert!(!is_meta_empty(&meta), "meta with source must not be empty");
}

#[test]
fn merge_route_makes_meta_non_empty() {
// Route must be visible in DEFAULT (non-verbose) output — merging it
Expand Down
2 changes: 2 additions & 0 deletions crates/ff-rdp-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@ fn dispatch_inner(
ref_id,
interactive,
critical,
native,
}) => {
let resolved_selector: Option<String> = if let Some(id) = ref_id.as_deref() {
Some(resolve_ref_via_daemon(cli, id)?)
Expand All @@ -772,6 +773,7 @@ fn dispatch_inner(
*max_chars,
effective_selector,
*interactive,
*native,
)
}
}
Expand Down
Loading
Loading