Bug 1887786 part 2: Implement the UIA SelectionItem pattern. r=morgan

Differential Revision: https://phabricator.services.mozilla.com/D208435
This commit is contained in:
James Teh 2024-04-26 01:08:57 +00:00
parent eeb9689caf
commit 330728d677
5 changed files with 240 additions and 2 deletions

View file

@ -77,6 +77,29 @@ async function testSelectionProps(id, selection, multiple, required) {
}
}
async function testSelectionItemProps(id, selected, container) {
await assignPyVarToUiaWithId(id);
await definePyVar("pattern", `getUiaPattern(${id}, "SelectionItem")`);
ok(await runPython(`bool(pattern)`), `${id} has SelectionItem pattern`);
is(
!!(await runPython(`pattern.CurrentIsSelected`)),
selected,
`${id} has correct IsSelected`
);
if (container) {
is(
await runPython(`pattern.CurrentSelectionContainer.CurrentAutomationId`),
container,
`${id} has correct SelectionContainer`
);
} else {
ok(
!(await runPython(`bool(pattern.CurrentSelectionContainer)`)),
`${id} has no SelectionContainer`
);
}
}
/**
* Test the Selection pattern.
*/
@ -125,3 +148,79 @@ addUiaTask(SNIPPET, async function testSelection(browser) {
await testPatternAbsent("button", "Selection");
});
/**
* Test the SelectionItem pattern.
*/
addUiaTask(SNIPPET, async function testSelection() {
await definePyVar("doc", `getDocUia()`);
await testPatternAbsent("selectList", "SelectionItem");
await testSelectionItemProps("sl1", true, "selectList");
await testSelectionItemProps("sl2", false, "selectList");
info("Calling Select on sl2");
await setUpWaitForUiaEvent("SelectionItem_ElementSelected", "sl2");
await runPython(`pattern.Select()`);
await waitForUiaEvent();
ok(true, "sl2 got ElementSelected event");
await testSelectionItemProps("sl1", false, "selectList");
await testSelectionItemProps("sl2", true, "selectList");
await testSelectionItemProps("sr1", false, "selectRequired");
await testSelectionItemProps("sm1", true, "selectMulti");
await testSelectionItemProps("sm2", false, "selectMulti");
info("Calling AddToSelection on sm2");
await setUpWaitForUiaEvent("SelectionItem_ElementAddedToSelection", "sm2");
await runPython(`pattern.AddToSelection()`);
await waitForUiaEvent();
ok(true, "sm2 got ElementAddedToSelection event");
await testSelectionItemProps("sm2", true, "selectMulti");
await testSelectionItemProps("sm3", true, "selectMulti");
info("Calling RemoveFromSelection on sm3");
await setUpWaitForUiaEvent(
"SelectionItem_ElementRemovedFromSelection",
"sm3"
);
await runPython(`pattern.RemoveFromSelection()`);
await waitForUiaEvent();
ok(true, "sm3 got ElementRemovedFromSelection event");
await testSelectionItemProps("sm3", false, "selectMulti");
await testSelectionItemProps("t1", false, "tablist");
await testSelectionItemProps("t2", true, "tablist");
// The IA2 -> UIA proxy doesn't expose the SelectionItem pattern on grid
// cells.
if (gIsUiaEnabled) {
await testSelectionItemProps("g1", false, "grid");
await testSelectionItemProps("g2", true, "grid");
}
await testSelectionItemProps("r1", true, null);
await testSelectionItemProps("r2", false, null);
// The IA2 -> UIA proxy doesn't fire correct events for radio buttons.
if (gIsUiaEnabled) {
info("Calling Select on r2");
await setUpWaitForUiaEvent("SelectionItem_ElementSelected", "r2");
await runPython(`pattern.Select()`);
await waitForUiaEvent();
ok(true, "r2 got ElementSelected event");
await testSelectionItemProps("r1", false, null);
await testSelectionItemProps("r2", true, null);
info("Calling RemoveFromSelection on r2");
await testPythonRaises(
`pattern.RemoveFromSelection()`,
"RemoveFromSelection failed on r2"
);
}
await testPatternAbsent("m1", "SelectionItem");
// The IA2 -> UIA proxy doesn't expose the SelectionItem pattern for radio
// menu items.
if (gIsUiaEnabled) {
await testSelectionItemProps("m2", false, null);
await testSelectionItemProps("m3", true, null);
}
await testPatternAbsent("button", "SelectionItem");
});

View file

@ -27,6 +27,7 @@ addUiaTask(
<button id="button">button</button>
<p id="p">p</p>
<input id="checkbox" type="checkbox">
<input id="radio" type="radio">
`,
async function testInvoke() {
await definePyVar("doc", `getDocUia()`);
@ -54,6 +55,8 @@ addUiaTask(
// Check boxes expose the Toggle pattern, so they should not expose the
// Invoke pattern.
await testPatternAbsent("checkbox", "Invoke");
// Ditto for radio buttons.
await testPatternAbsent("radio", "Invoke");
}
}
);

View file

@ -129,6 +129,7 @@ void a11y::PlatformShowHideEvent(Accessible* aTarget, Accessible*, bool aInsert,
void a11y::PlatformSelectionEvent(Accessible* aTarget, Accessible*,
uint32_t aType) {
MsaaAccessible::FireWinEvent(aTarget, aType);
uiaRawElmProvider::RaiseUiaEventForGeckoEvent(aTarget, aType);
}
static bool GetInstantiatorExecutable(const DWORD aPid,

View file

@ -54,6 +54,11 @@ static ExpandCollapseState ToExpandCollapseState(uint64_t aState) {
return ExpandCollapseState_LeafNode;
}
static bool IsRadio(Accessible* aAcc) {
role r = aAcc->Role();
return r == roles::RADIOBUTTON || r == roles::RADIO_MENU_ITEM;
}
////////////////////////////////////////////////////////////////////////////////
// uiaRawElmProvider
////////////////////////////////////////////////////////////////////////////////
@ -87,6 +92,17 @@ void uiaRawElmProvider::RaiseUiaEventForGeckoEvent(Accessible* aAcc,
case nsIAccessibleEvent::EVENT_NAME_CHANGE:
property = UIA_NamePropertyId;
break;
case nsIAccessibleEvent::EVENT_SELECTION:
::UiaRaiseAutomationEvent(uia, UIA_SelectionItem_ElementSelectedEventId);
return;
case nsIAccessibleEvent::EVENT_SELECTION_ADD:
::UiaRaiseAutomationEvent(
uia, UIA_SelectionItem_ElementAddedToSelectionEventId);
return;
case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
::UiaRaiseAutomationEvent(
uia, UIA_SelectionItem_ElementRemovedFromSelectionEventId);
return;
case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
::UiaRaiseAutomationEvent(uia, UIA_Selection_InvalidatedEventId);
return;
@ -129,6 +145,13 @@ void uiaRawElmProvider::RaiseUiaEventForStateChange(Accessible* aAcc,
_variant_t newVal;
switch (aState) {
case states::CHECKED:
if (aEnabled && IsRadio(aAcc)) {
::UiaRaiseAutomationEvent(uia,
UIA_SelectionItem_ElementSelectedEventId);
return;
}
// For other checkable things, the Toggle pattern is used.
[[fallthrough]];
case states::MIXED:
case states::PRESSED:
property = UIA_ToggleToggleStatePropertyId;
@ -177,6 +200,8 @@ uiaRawElmProvider::QueryInterface(REFIID aIid, void** aInterface) {
*aInterface = static_cast<IRangeValueProvider*>(this);
} else if (aIid == IID_IScrollItemProvider) {
*aInterface = static_cast<IScrollItemProvider*>(this);
} else if (aIid == IID_ISelectionItemProvider) {
*aInterface = static_cast<ISelectionItemProvider*>(this);
} else if (aIid == IID_ISelectionProvider) {
*aInterface = static_cast<ISelectionProvider*>(this);
} else if (aIid == IID_IToggleProvider) {
@ -302,7 +327,7 @@ uiaRawElmProvider::GetPatternProvider(
// the same behavior is not exposed through another control pattern
// provider".
if (acc->ActionCount() > 0 && !HasTogglePattern() &&
!HasExpandCollapsePattern()) {
!HasExpandCollapsePattern() && !HasSelectionItemPattern()) {
RefPtr<IInvokeProvider> invoke = this;
invoke.forget(aPatternProvider);
}
@ -318,6 +343,12 @@ uiaRawElmProvider::GetPatternProvider(
scroll.forget(aPatternProvider);
return S_OK;
}
case UIA_SelectionItemPatternId:
if (HasSelectionItemPattern()) {
RefPtr<ISelectionItemProvider> item = this;
item.forget(aPatternProvider);
}
return S_OK;
case UIA_SelectionPatternId:
// According to the UIA documentation, radio button groups should support
// the Selection pattern. However:
@ -989,6 +1020,86 @@ uiaRawElmProvider::get_IsSelectionRequired(__RPC__out BOOL* aRetVal) {
return S_OK;
}
// ISelectionItemProvider methods
STDMETHODIMP
uiaRawElmProvider::Select() {
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
if (IsRadio(acc)) {
acc->DoAction(0);
} else {
acc->TakeSelection();
}
return S_OK;
}
STDMETHODIMP
uiaRawElmProvider::AddToSelection() {
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
if (IsRadio(acc)) {
acc->DoAction(0);
} else {
acc->SetSelected(true);
}
return S_OK;
}
STDMETHODIMP
uiaRawElmProvider::RemoveFromSelection() {
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
if (IsRadio(acc)) {
return UIA_E_INVALIDOPERATION;
}
acc->SetSelected(false);
return S_OK;
}
STDMETHODIMP
uiaRawElmProvider::get_IsSelected(__RPC__out BOOL* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
if (IsRadio(acc)) {
*aRetVal = acc->State() & states::CHECKED;
} else {
*aRetVal = acc->State() & states::SELECTED;
}
return S_OK;
}
STDMETHODIMP
uiaRawElmProvider::get_SelectionContainer(
__RPC__deref_out_opt IRawElementProviderSimple** aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
*aRetVal = nullptr;
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
Accessible* container = nsAccUtils::GetSelectableContainer(acc, acc->State());
if (!container) {
return E_FAIL;
}
RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(container);
uia.forget(aRetVal);
return S_OK;
}
// Private methods
bool uiaRawElmProvider::IsControl() {
@ -1105,6 +1216,14 @@ RefPtr<Interface> uiaRawElmProvider::GetPatternFromDerived() {
return derived;
}
bool uiaRawElmProvider::HasSelectionItemPattern() {
Accessible* acc = Acc();
MOZ_ASSERT(acc);
// In UIA, radio buttons and radio menu items are exposed as selected or
// unselected.
return acc->State() & states::SELECTABLE || IsRadio(acc);
}
SAFEARRAY* a11y::AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs) {
if (aAccs.IsEmpty()) {
// The UIA documentation is unclear about this, but the UIA client

View file

@ -33,7 +33,8 @@ class uiaRawElmProvider : public IAccessibleEx,
public IScrollItemProvider,
public IValueProvider,
public IRangeValueProvider,
public ISelectionProvider {
public ISelectionProvider,
public ISelectionItemProvider {
public:
static constexpr enum ProviderOptions kProviderOptions =
static_cast<enum ProviderOptions>(ProviderOptions_ServerSideProvider |
@ -161,6 +162,20 @@ class uiaRawElmProvider : public IAccessibleEx,
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_IsSelectionRequired(
/* [retval][out] */ __RPC__out BOOL* aRetVal);
// ISelectionItemProvider methods
virtual HRESULT STDMETHODCALLTYPE Select(void);
virtual HRESULT STDMETHODCALLTYPE AddToSelection(void);
virtual HRESULT STDMETHODCALLTYPE RemoveFromSelection(void);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_IsSelected(
/* [retval][out] */ __RPC__out BOOL* aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_SelectionContainer(
/* [retval][out] */ __RPC__deref_out_opt IRawElementProviderSimple**
aRetVal);
private:
Accessible* Acc() const;
bool IsControl();
@ -170,6 +185,7 @@ class uiaRawElmProvider : public IAccessibleEx,
bool HasValuePattern() const;
template <class Derived, class Interface>
RefPtr<Interface> GetPatternFromDerived();
bool HasSelectionItemPattern();
};
SAFEARRAY* AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs);