diff --git a/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs b/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs
index 37780d481..6e7225996 100644
--- a/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs
+++ b/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs
@@ -403,6 +403,89 @@ this is the inner content
}));
}
+ [Test, PuppeteerTest("accessibility.spec", "Accessibility", "should capture new accessibility properties and not prune them")]
+ public async Task ShouldCaptureNewAccessibilityPropertiesAndNotPruneThem()
+ {
+ await Page.SetContentAsync(@"
+
This is an alert
+
+ This is polite live region
+
+
+ Modal content
+
+ Error message
+
+ Additional details
+ Element with details
+ ");
+
+ var snapshot = await Page.Accessibility.SnapshotAsync();
+
+ Assert.That(snapshot.Role, Is.EqualTo("RootWebArea"));
+ Assert.That(snapshot.Children.Length, Is.GreaterThanOrEqualTo(8));
+
+ // alert node with busy, live, atomic
+ var alertNode = snapshot.Children[0];
+ Assert.That(alertNode.Role, Is.EqualTo("alert"));
+ Assert.That(alertNode.Name, Is.EqualTo(string.Empty));
+ Assert.That(alertNode.Busy, Is.True);
+ Assert.That(alertNode.Live, Is.EqualTo("assertive"));
+ Assert.That(alertNode.Atomic, Is.True);
+ Assert.That(alertNode.Children, Has.Length.EqualTo(1));
+ Assert.That(alertNode.Children[0].Role, Is.EqualTo("StaticText"));
+ Assert.That(alertNode.Children[0].Name, Is.EqualTo("This is an alert"));
+
+ // polite live region with atomic and relevant
+ var liveRegionNode = snapshot.Children[1];
+ Assert.That(liveRegionNode.Role, Is.EqualTo("generic"));
+ Assert.That(liveRegionNode.Name, Is.EqualTo(string.Empty));
+ Assert.That(liveRegionNode.Live, Is.EqualTo("polite"));
+ Assert.That(liveRegionNode.Atomic, Is.True);
+ Assert.That(liveRegionNode.Relevant, Is.EqualTo("additions text"));
+ Assert.That(liveRegionNode.Children, Has.Length.EqualTo(1));
+ Assert.That(liveRegionNode.Children[0].Role, Is.EqualTo("StaticText"));
+ Assert.That(liveRegionNode.Children[0].Name, Is.EqualTo("This is polite live region"));
+
+ // dialog with modal and roledescription
+ var dialogNode = snapshot.Children[2];
+ Assert.That(dialogNode.Role, Is.EqualTo("dialog"));
+ Assert.That(dialogNode.Name, Is.EqualTo(string.Empty));
+ Assert.That(dialogNode.Modal, Is.True);
+ Assert.That(dialogNode.RoleDescription, Is.EqualTo("My Modal"));
+ Assert.That(dialogNode.Children, Has.Length.EqualTo(1));
+ Assert.That(dialogNode.Children[0].Role, Is.EqualTo("StaticText"));
+ Assert.That(dialogNode.Children[0].Name, Is.EqualTo("Modal content"));
+
+ // Error message static text
+ Assert.That(snapshot.Children[3].Role, Is.EqualTo("StaticText"));
+ Assert.That(snapshot.Children[3].Name, Is.EqualTo("Error message"));
+
+ // input with invalid and errormessage
+ var inputNode = snapshot.Children[4];
+ Assert.That(inputNode.Role, Is.EqualTo("textbox"));
+ Assert.That(inputNode.Value, Is.EqualTo("invalid input"));
+ Assert.That(inputNode.Invalid, Is.EqualTo("true"));
+ Assert.That(inputNode.Errormessage, Is.EqualTo("error"));
+
+ // Additional details static text
+ Assert.That(snapshot.Children[5].Role, Is.EqualTo("StaticText"));
+ Assert.That(snapshot.Children[5].Name, Is.EqualTo("Additional details"));
+
+ // element with details
+ var detailsNode = snapshot.Children[6];
+ Assert.That(detailsNode.Role, Is.EqualTo("generic"));
+ Assert.That(detailsNode.Details, Is.EqualTo("details"));
+ Assert.That(detailsNode.Children, Has.Length.EqualTo(1));
+ Assert.That(detailsNode.Children[0].Role, Is.EqualTo("StaticText"));
+ Assert.That(detailsNode.Children[0].Name, Is.EqualTo("Element with details"));
+
+ // element with description only (no name)
+ var descriptionNode = snapshot.Children[7];
+ Assert.That(descriptionNode.Role, Is.EqualTo("generic"));
+ Assert.That(descriptionNode.Description, Is.EqualTo("This is a description"));
+ }
+
private SerializedAXNode FindFocusedNode(SerializedAXNode serializedAXNode)
{
if (serializedAXNode.Focused)
diff --git a/lib/PuppeteerSharp/PageAccessibility/AXNode.cs b/lib/PuppeteerSharp/PageAccessibility/AXNode.cs
index 62dd85495..c78cb8f8f 100644
--- a/lib/PuppeteerSharp/PageAccessibility/AXNode.cs
+++ b/lib/PuppeteerSharp/PageAccessibility/AXNode.cs
@@ -15,7 +15,14 @@ internal class AXNode
private readonly bool _richlyEditable;
private readonly bool _editable;
private readonly bool _hidden;
+ private readonly bool _busy;
+ private readonly bool _modal;
+ private readonly bool _hasErrormessage;
+ private readonly bool _hasDetails;
private readonly string _role;
+ private readonly string _description;
+ private readonly string _roledescription;
+ private readonly string _live;
private readonly bool _ignored;
private bool? _cachedHasFocusableChild;
@@ -23,14 +30,21 @@ private AXNode(AccessibilityGetFullAXTreeResponse.AXTreeNode payload)
{
Payload = payload;
- _name = payload.Name != null ? payload.Name.Value.ToObject() : string.Empty;
_role = payload.Role != null ? payload.Role.Value.ToObject() : "Unknown";
_ignored = payload.Ignored;
+ _name = payload.Name != null ? payload.Name.Value.ToObject() : string.Empty;
+ _description = payload.Description != null ? payload.Description.Value.ToObject() : null;
_richlyEditable = payload.Properties?.FirstOrDefault(p => p.Name == "editable")?.Value.Value.ToObject() == "richtext";
_editable |= _richlyEditable;
_hidden = payload.Properties?.FirstOrDefault(p => p.Name == "hidden")?.Value.Value.ToObject() == true;
Focusable = payload.Properties?.FirstOrDefault(p => p.Name == "focusable")?.Value.Value.ToObject() == true;
+ _busy = GetBooleanProperty(payload, "busy");
+ _live = payload.Properties?.FirstOrDefault(p => p.Name == "live")?.Value.Value.ToObject();
+ _modal = GetBooleanProperty(payload, "modal");
+ _roledescription = payload.Properties?.FirstOrDefault(p => p.Name == "roledescription")?.Value.Value.ToObject();
+ _hasErrormessage = payload.Properties?.Any(p => p.Name == "errormessage") == true;
+ _hasDetails = payload.Properties?.Any(p => p.Name == "details") == true;
}
public List Children { get; } = new();
@@ -168,7 +182,14 @@ internal bool IsInteresting(bool insideControl)
return false;
}
- if (Focusable || _richlyEditable)
+ if (Focusable ||
+ _richlyEditable ||
+ _busy ||
+ (_live != null && _live != "off") ||
+ _modal ||
+ _hasErrormessage ||
+ _hasDetails ||
+ _roledescription != null)
{
return true;
}
@@ -185,7 +206,7 @@ internal bool IsInteresting(bool insideControl)
return false;
}
- return IsLeafNode() && !string.IsNullOrEmpty(_name);
+ return IsLeafNode() && (!string.IsNullOrEmpty(_name) || !string.IsNullOrEmpty(_description));
}
internal SerializedAXNode Serialize()
@@ -236,6 +257,8 @@ internal SerializedAXNode Serialize()
Readonly = properties.GetValue("readonly")?.ToObject() ?? false,
Required = properties.GetValue("required")?.ToObject() ?? false,
Selected = properties.GetValue("selected")?.ToObject() ?? false,
+ Busy = GetBooleanPropertyValue(properties, "busy"),
+ Atomic = GetBooleanPropertyValue(properties, "atomic"),
Checked = GetCheckedState(properties.GetValue("checked")?.ToObject()),
Pressed = GetCheckedState(properties.GetValue("pressed")?.ToObject()),
Level = properties.GetValue("level")?.ToObject() ?? 0,
@@ -245,11 +268,48 @@ internal SerializedAXNode Serialize()
HasPopup = GetIfNotFalse(properties.GetValue("haspopup")?.ToObject()),
Invalid = GetIfNotFalse(properties.GetValue("invalid")?.ToObject()),
Orientation = GetIfNotFalse(properties.GetValue("orientation")?.ToObject()),
+ Live = GetIfNotFalse(properties.GetValue("live")?.ToObject()),
+ Relevant = GetIfNotFalse(properties.GetValue("relevant")?.ToObject()),
+ Errormessage = GetIfNotFalse(properties.GetValue("errormessage")?.ToObject()),
+ Details = GetIfNotFalse(properties.GetValue("details")?.ToObject()),
};
return node;
}
+ private static bool GetBooleanProperty(AccessibilityGetFullAXTreeResponse.AXTreeNode payload, string propertyName)
+ {
+ var prop = payload.Properties?.FirstOrDefault(p => p.Name == propertyName);
+ if (prop == null)
+ {
+ return false;
+ }
+
+ var element = prop.Value.Value;
+ return element.ValueKind switch
+ {
+ JsonValueKind.True => true,
+ JsonValueKind.Number => element.GetInt32() != 0,
+ _ => false,
+ };
+ }
+
+ private static bool GetBooleanPropertyValue(Dictionary properties, string key)
+ {
+ var element = properties.GetValue(key);
+ if (element == null)
+ {
+ return false;
+ }
+
+ return element.Value.ValueKind switch
+ {
+ JsonValueKind.True => true,
+ JsonValueKind.Number => element.Value.GetInt32() != 0,
+ _ => false,
+ };
+ }
+
private bool IsPlainTextField()
=> !_richlyEditable && (_editable || _role == "textbox" || _role == "ComboBox" || _role == "searchbox");
diff --git a/lib/PuppeteerSharp/PageAccessibility/SerializedAXNode.cs b/lib/PuppeteerSharp/PageAccessibility/SerializedAXNode.cs
index f679f229f..1fc07724a 100644
--- a/lib/PuppeteerSharp/PageAccessibility/SerializedAXNode.cs
+++ b/lib/PuppeteerSharp/PageAccessibility/SerializedAXNode.cs
@@ -138,6 +138,36 @@ public class SerializedAXNode : IEquatable
///
public string Orientation { get; set; }
+ ///
+ /// Whether the node is busy.
+ ///
+ public bool Busy { get; set; }
+
+ ///
+ /// The live status of the node.
+ ///
+ public string Live { get; set; }
+
+ ///
+ /// Whether the live region is atomic.
+ ///
+ public bool Atomic { get; set; }
+
+ ///
+ /// The relevant changes for the live region.
+ ///
+ public string Relevant { get; set; }
+
+ ///
+ /// The error message for the node.
+ ///
+ public string Errormessage { get; set; }
+
+ ///
+ /// The details for the node.
+ ///
+ public string Details { get; set; }
+
///
/// Child nodes of this node, if any.
///
@@ -158,6 +188,12 @@ public bool Equals(SerializedAXNode other)
AutoComplete == other.AutoComplete &&
HasPopup == other.HasPopup &&
Orientation == other.Orientation &&
+ Busy == other.Busy &&
+ Live == other.Live &&
+ Atomic == other.Atomic &&
+ Relevant == other.Relevant &&
+ Errormessage == other.Errormessage &&
+ Details == other.Details &&
Disabled == other.Disabled &&
Expanded == other.Expanded &&
Focused == other.Focused &&
@@ -189,6 +225,12 @@ public override int GetHashCode()
AutoComplete.GetHashCode() ^
HasPopup.GetHashCode() ^
Orientation.GetHashCode() ^
+ Busy.GetHashCode() ^
+ Live.GetHashCode() ^
+ Atomic.GetHashCode() ^
+ Relevant.GetHashCode() ^
+ Errormessage.GetHashCode() ^
+ Details.GetHashCode() ^
Disabled.GetHashCode() ^
Expanded.GetHashCode() ^
Focused.GetHashCode() ^