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() ^