Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for accessibility custom actions #1295

Merged
merged 12 commits into from
Aug 1, 2024

Conversation

RoyalPineapple
Copy link
Contributor

@RoyalPineapple RoyalPineapple commented Jul 9, 2024

Custom actions allow developers to provide custom interactions that can be initiated through assistive technologies like VoiceOver and Switch Control. For example, VoiceOver lets users access actions quickly using the Actions rotor.
Custom actions are relied on heavily by SwiftUI's .accessibilityElement(children:.combine) view modifier.

This PR introduces two methods on KIFUIViewTestActor to enable validation and activation of an element's custom actions within KIF tests.

- (instancetype)usingCustomActionWithName:(NSString *)name; adds a predicate that searches elements for one that provides a custom action with a specified name.

- (void)activateCustomActionWithName:(NSString *)name; first performs a search for an element that provides a custom action with a specified name and then activates the action.

@RoyalPineapple RoyalPineapple marked this pull request as ready for review July 9, 2024 19:53
@RoyalPineapple RoyalPineapple changed the title [DNM][DNR] adding support for accessibility custom actions Adding support for accessibility custom actions Jul 9, 2024
@RoyalPineapple
Copy link
Contributor Author

RoyalPineapple commented Jul 16, 2024

@justinseanmartin What do you think of this proposed API? We're going to start relying on Custom Actions much more frequently in Market and im expecting I'll need this capability a fair amount to update our existing tests.

Copy link
Contributor

@justinseanmartin justinseanmartin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly NITs or questions, but I think there probably are some minor changes to be made at least for prefixing the method in the category, the autoreleasepool and potentially the respondsToSelector check.

As to the question about the API. overall it seems good to me. I'm not sure if there is a scenario where we'll want to use usingCustomActionWithName: directly, as opposed to it being an implicit part of the action. I'd consider removing that from the public header if there isn't a scenario you're thinking of where we'll need to use that. I don't have a strong feeling on that though. It just seems kind of weird to have a [[viewTester usingCustomActionWithName:@"action"] waitForAppearance] or maybe even wierder [[viewTester usingCustomActionWithName:@"action"] tap]. We also don't have any tests exercising usingCustomActionWithName directly, which feels potentially like another indicator that we might not need that exposed?

- (instancetype)usingCustomActionWithName:(NSString *)name
{
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
NSArray *actions = [evaluatedObject accessibilityCustomActions];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every element that we iterate over in the view & accessibility element hierarchy is guaranteed to respond to this selector? Or should we add a respondsToSelector: check?

Copy link
Contributor Author

@RoyalPineapple RoyalPineapple Jul 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a property on NSObject defined thusly in UIAccessibility.h
@property (nullable, nonatomic, strong) NSArray <UIAccessibilityCustomAction *> *accessibilityCustomActions API_AVAILABLE(ios(8.0));

Though a respondsToSelector: check couldn't hurt.

Comment on lines 163 to 172
for (UIAccessibilityCustomAction *action in actions) {
NSString *actionName = [action name];
if ([actionName isKindOfClass:[NSAttributedString class]]) {
actionName = [(NSAttributedString *)actionName string];
}

if ([actionName isEqualToString: name]) {
return true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Up to you, but maybe worth a helper method for getting the specified action by name? Its identical here and below and might make these methods slightly simpler.

@@ -385,6 +406,31 @@ - (void)swipeFromEdge:(UIRectEdge)edge
[self.actor swipeFromEdge:edge];
}

- (void)activateCustomActionWithName:(NSString *)name;
{
KIFUIObject *found = [[self usingCustomActionWithName:name] _predicateSearchWithRequiresMatch:YES mustBeTappable:NO];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally wrap the body of these methods that return a KIFUIObject in an @autoreleasepool block so as to not accidentally extend the lifetime of the view object beyond this action.

Sources/KIF/Classes/KIFUIViewTestActor.m Outdated Show resolved Hide resolved

@interface UIAccessibilityCustomAction (KIFAdditions)

- (BOOL)activate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a prefix to avoid collisions, e.g. KIF_activate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh man, i honestly do love objective c

[invocation getReturnValue:&returnValue];
return returnValue;
}
return NO;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to raise if we attempted to perform an action on an element that says it supports it, but hasn't implemented the selector? I'd kind of expect this to crash or raise an error in some way that is distinct from the selector returning false from the action.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, nice idea

}

if ([actionName isEqualToString: name]) {
if([action activate]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do VoiceOver and other accessibility tools respond if a custom action returns false? Are there valid use cases where a custom action might want to return false and that be a valid test scenario rather than treating it as a test failure? We don't need to extend to support that right now, but it seemed worth some thought/discussion.

Is it reasonable that a custom action might fail in some cases? Or is the expectation that custom actions should generally only be present on an element if they're expected to be able to be performed? Looking at the Firefox iOS codebase as an example, it seems like they filter the actions to ones that are expected to be usable in the current context, and then always return true. That's just one example though.

Copy link
Contributor Author

@RoyalPineapple RoyalPineapple Jul 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theres no official dogma on this particular question, but as we interpret the default behavior of UIKit and SwiftUI as guidance it would suggest that custom actions that are known to fail should be omitted entirely. Disabled buttons for example.

From the point of view of the person in front of the device, a failed action will play an additional "error" sound when activated.

You're right, we should consider the case where an action is expected to occasionally fail and allow for testing that assumption.

I've added an "expectedResult boolean" to allow this.

Copy link
Contributor

@justinseanmartin justinseanmartin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@RoyalPineapple RoyalPineapple merged commit 8757ba8 into master Aug 1, 2024
4 of 6 checks passed
SimoncelloCT added a commit to SimoncelloCT/KIF that referenced this pull request Aug 21, 2024
* silencing non-exhaustive switch warning (kif-framework#1297)

silencing non-exhaustive switch warning

* Adding support for accessibility custom actions (kif-framework#1295)

* adding support for accessibility custom actions

* introduce tryFindingViewInFrame
- avoid scroll when looking for a view visibility

* remove wrong comment

* improve comments

* add missing disableScroll argument

* introduce usingCurrentFrame() method on KIFUIViewTestActor to disable automatic scroll if needed

* fixes:
- fix wrong parameter name in comments
- add missing methods
- add missing things that prevent us to build for tests

* fix to scrollView behavior:
- with scrollDisabled = YES it now consider only views visible in the current frame, as it was done with TableView and CollectionViews
- make explicit the scrollDisabled argument in recursive calls = NO when we are inside the scrolling section

* add new integration tests for tryFindingViewInFrameWithAccessibilityIdentifier() and usingCurrentFrame()

* Fix bug that was considering an occluded view as tappable

* simplify boolean check and add integration test for broken case

* Revert "simplify boolean check and add integration test for broken case"

This reverts commit 6827225.

* simplify boolean logic of the tappable check

* add missing fromRootView argument

Co-authored-by: Justin Martin <jmartin@squareup.com>

* Change duplicated accessibilityLabel that was causing flakiness for 2 tests

---------

Co-authored-by: Alex Odawa <aodawa@squareup.com>
Co-authored-by: Simone Scionti <simone.scionti@qonto.com>
Co-authored-by: Justin Martin <jmartin@squareup.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants