Bug 1887780 part 2: Implement the UIA Value pattern. r=morgan

Differential Revision: https://phabricator.services.mozilla.com/D206449
This commit is contained in:
James Teh 2024-04-05 00:19:23 +00:00
parent 0d51bb4e69
commit e84478d977
3 changed files with 258 additions and 4 deletions

View file

@ -283,3 +283,163 @@ addUiaTask(
testStates(button, 0, 0, STATE_OFFSCREEN);
}
);
/**
* Test the Value pattern.
*/
addUiaTask(
`
<input id="text" value="before">
<input id="textRo" readonly value="textRo">
<input id="textDis" disabled value="textDis">
<select id="select"><option selected>a</option><option>b</option></select>
<progress id="progress" value="0.5"></progress>
<input id="range" type="range" aria-valuetext="02:00:00">
<a id="link" href="https://example.com/">Link</a>
<div id="ariaTextbox" contenteditable role="textbox">before</div>
<button id="button">button</button>
`,
async function testValue() {
await definePyVar("doc", `getDocUia()`);
await assignPyVarToUiaWithId("text");
await definePyVar("pattern", `getUiaPattern(text, "Value")`);
ok(await runPython(`bool(pattern)`), "text has Value pattern");
ok(
!(await runPython(`pattern.CurrentIsReadOnly`)),
"text has IsReadOnly false"
);
is(
await runPython(`pattern.CurrentValue`),
"before",
"text has correct Value"
);
info("SetValue on text");
await setUpWaitForUiaPropEvent("ValueValue", "text");
await runPython(`pattern.SetValue("after")`);
await waitForUiaEvent();
is(
await runPython(`pattern.CurrentValue`),
"after",
"text has correct Value"
);
await assignPyVarToUiaWithId("textRo");
await definePyVar("pattern", `getUiaPattern(textRo, "Value")`);
ok(await runPython(`bool(pattern)`), "textRo has Value pattern");
ok(
await runPython(`pattern.CurrentIsReadOnly`),
"textRo has IsReadOnly true"
);
is(
await runPython(`pattern.CurrentValue`),
"textRo",
"textRo has correct Value"
);
info("SetValue on textRo");
await testPythonRaises(
`pattern.SetValue("after")`,
"SetValue on textRo failed"
);
await assignPyVarToUiaWithId("textDis");
await definePyVar("pattern", `getUiaPattern(textDis, "Value")`);
ok(await runPython(`bool(pattern)`), "textDis has Value pattern");
ok(
!(await runPython(`pattern.CurrentIsReadOnly`)),
"textDis has IsReadOnly false"
);
is(
await runPython(`pattern.CurrentValue`),
"textDis",
"textDis has correct Value"
);
// The IA2 -> UIA proxy doesn't fail SetValue for a disabled element.
if (gIsUiaEnabled) {
info("SetValue on textDis");
await testPythonRaises(
`pattern.SetValue("after")`,
"SetValue on textDis failed"
);
}
await assignPyVarToUiaWithId("select");
await definePyVar("pattern", `getUiaPattern(select, "Value")`);
ok(await runPython(`bool(pattern)`), "select has Value pattern");
ok(
!(await runPython(`pattern.CurrentIsReadOnly`)),
"select has IsReadOnly false"
);
is(
await runPython(`pattern.CurrentValue`),
"a",
"select has correct Value"
);
info("SetValue on select");
await testPythonRaises(
`pattern.SetValue("b")`,
"SetValue on select failed"
);
await assignPyVarToUiaWithId("progress");
await definePyVar("pattern", `getUiaPattern(progress, "Value")`);
ok(await runPython(`bool(pattern)`), "progress has Value pattern");
// Gecko a11y doesn't treat progress bars as read only, but it probably
// should.
todo(
await runPython(`pattern.CurrentIsReadOnly`),
"progress has IsReadOnly true"
);
is(
await runPython(`pattern.CurrentValue`),
"50%",
"progress has correct Value"
);
info("SetValue on progress");
await testPythonRaises(
`pattern.SetValue("60%")`,
"SetValue on progress failed"
);
await assignPyVarToUiaWithId("range");
await definePyVar("pattern", `getUiaPattern(range, "Value")`);
ok(await runPython(`bool(pattern)`), "range has Value pattern");
is(
await runPython(`pattern.CurrentValue`),
"02:00:00",
"range has correct Value"
);
await assignPyVarToUiaWithId("link");
await definePyVar("pattern", `getUiaPattern(link, "Value")`);
ok(await runPython(`bool(pattern)`), "link has Value pattern");
is(
await runPython(`pattern.CurrentValue`),
"https://example.com/",
"link has correct Value"
);
await assignPyVarToUiaWithId("ariaTextbox");
await definePyVar("pattern", `getUiaPattern(ariaTextbox, "Value")`);
ok(await runPython(`bool(pattern)`), "ariaTextbox has Value pattern");
ok(
!(await runPython(`pattern.CurrentIsReadOnly`)),
"ariaTextbox has IsReadOnly false"
);
is(
await runPython(`pattern.CurrentValue`),
"before",
"ariaTextbox has correct Value"
);
info("SetValue on ariaTextbox");
await setUpWaitForUiaPropEvent("ValueValue", "ariaTextbox");
await runPython(`pattern.SetValue("after")`);
await waitForUiaEvent();
is(
await runPython(`pattern.CurrentValue`),
"after",
"ariaTextbox has correct Value"
);
await testPatternAbsent("button", "Value");
}
);

View file

@ -70,6 +70,10 @@ void uiaRawElmProvider::RaiseUiaEventForGeckoEvent(Accessible* aAcc,
return;
}
PROPERTYID property = 0;
_variant_t newVal;
bool gotNewVal = false;
// For control pattern properties, we can't use GetPropertyValue. In those
// cases, we must set newVal appropriately and set gotNewVal to true.
switch (aGeckoEvent) {
case nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE:
property = UIA_FullDescriptionPropertyId;
@ -80,13 +84,20 @@ void uiaRawElmProvider::RaiseUiaEventForGeckoEvent(Accessible* aAcc,
case nsIAccessibleEvent::EVENT_NAME_CHANGE:
property = UIA_NamePropertyId;
break;
case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
property = UIA_ValueValuePropertyId;
newVal.vt = VT_BSTR;
uia->get_Value(&newVal.bstrVal);
gotNewVal = true;
break;
}
// Don't pointlessly query the property value if no UIA clients are listening.
if (property && ::UiaClientsAreListening()) {
// We can't get the old value. Thankfully, clients don't seem to need it.
_variant_t oldVal;
_variant_t newVal;
uia->GetPropertyValue(property, &newVal);
if (!gotNewVal) {
// This isn't a pattern property, so we can use GetPropertyValue.
uia->GetPropertyValue(property, &newVal);
}
::UiaRaiseAutomationPropertyChangedEvent(uia, property, oldVal, newVal);
}
}
@ -154,6 +165,8 @@ uiaRawElmProvider::QueryInterface(REFIID aIid, void** aInterface) {
*aInterface = static_cast<IScrollItemProvider*>(this);
} else if (aIid == IID_IToggleProvider) {
*aInterface = static_cast<IToggleProvider*>(this);
} else if (aIid == IID_IValueProvider) {
*aInterface = static_cast<IValueProvider*>(this);
} else {
return E_NOINTERFACE;
}
@ -278,6 +291,12 @@ uiaRawElmProvider::GetPatternProvider(
toggle.forget(aPatternProvider);
}
return S_OK;
case UIA_ValuePatternId:
if (HasValuePattern()) {
RefPtr<IValueProvider> value = this;
value.forget(aPatternProvider);
}
return S_OK;
}
return S_OK;
}
@ -678,6 +697,58 @@ MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP uiaRawElmProvider::ScrollIntoView() {
return S_OK;
}
// IValueProvider methods
STDMETHODIMP
uiaRawElmProvider::SetValue(__RPC__in LPCWSTR aVal) {
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
HyperTextAccessibleBase* ht = acc->AsHyperTextBase();
if (!ht || !acc->IsTextRole()) {
return UIA_E_INVALIDOPERATION;
}
if (acc->State() & (states::READONLY | states::UNAVAILABLE)) {
return UIA_E_INVALIDOPERATION;
}
nsAutoString text(aVal);
ht->ReplaceText(text);
return S_OK;
}
STDMETHODIMP
uiaRawElmProvider::get_Value(__RPC__deref_out_opt BSTR* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
*aRetVal = nullptr;
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
nsAutoString value;
acc->Value(value);
*aRetVal = ::SysAllocStringLen(value.get(), value.Length());
if (!*aRetVal) {
return E_OUTOFMEMORY;
}
return S_OK;
}
STDMETHODIMP
uiaRawElmProvider::get_IsReadOnly(__RPC__out BOOL* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = acc->State() & states::READONLY;
return S_OK;
}
// Private methods
bool uiaRawElmProvider::IsControl() {
@ -772,3 +843,14 @@ bool uiaRawElmProvider::HasExpandCollapsePattern() {
MOZ_ASSERT(acc);
return acc->State() & (states::EXPANDABLE | states::HASPOPUP);
}
bool uiaRawElmProvider::HasValuePattern() const {
Accessible* acc = Acc();
MOZ_ASSERT(acc);
if (acc->HasNumericValue() || acc->IsCombobox() || acc->IsHTMLLink() ||
acc->IsTextField()) {
return true;
}
const nsRoleMapEntry* roleMapEntry = acc->ARIARoleMap();
return roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox);
}

View file

@ -25,7 +25,8 @@ class uiaRawElmProvider : public IAccessibleEx,
public IInvokeProvider,
public IToggleProvider,
public IExpandCollapseProvider,
public IScrollItemProvider {
public IScrollItemProvider,
public IValueProvider {
public:
static void RaiseUiaEventForGeckoEvent(Accessible* aAcc,
uint32_t aGeckoEvent);
@ -107,12 +108,23 @@ class uiaRawElmProvider : public IAccessibleEx,
// IScrollItemProvider
virtual HRESULT STDMETHODCALLTYPE ScrollIntoView(void);
// IValueProvider
virtual HRESULT STDMETHODCALLTYPE SetValue(
/* [in] */ __RPC__in LPCWSTR val);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Value(
/* [retval][out] */ __RPC__deref_out_opt BSTR* pRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_IsReadOnly(
/* [retval][out] */ __RPC__out BOOL* pRetVal);
private:
Accessible* Acc() const;
bool IsControl();
long GetControlType() const;
bool HasTogglePattern();
bool HasExpandCollapsePattern();
bool HasValuePattern() const;
};
} // namespace a11y