Add edit banner button UI automated test + accessibility improvements

This commit adds an automated UI test to check if the edit banner button
UI is clickable and not hidden behind a top bar or another invisible
element.

It also improves accessibility support for some elements on login and
top bar.

Changelog-Changed: Improved accessibility support on some elements
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2024-11-17 13:13:52 -08:00
parent e3105a90c5
commit 5c6e5ca2de
16 changed files with 218 additions and 94 deletions

View File

@@ -338,7 +338,6 @@
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CE6DEED27F7A08200C66700 /* Preview Assets.xcassets */; }; 4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CE6DEED27F7A08200C66700 /* Preview Assets.xcassets */; };
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEF727F7A08200C66700 /* damusTests.swift */; }; 4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEF727F7A08200C66700 /* damusTests.swift */; };
4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0127F7A08200C66700 /* damusUITests.swift */; }; 4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0127F7A08200C66700 /* damusUITests.swift */; };
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; }; 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; }; 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; }; 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
@@ -1057,6 +1056,11 @@
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5B2B77016700C59298 /* IAPProductStateView.swift */; }; D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5B2B77016700C59298 /* IAPProductStateView.swift */; };
D7100C5E2B7709ED00C59298 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; }; D7100C5E2B7709ED00C59298 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AC4CB2BA8E3480076268E /* VisibilityTracker.swift */; }; D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AC4CB2BA8E3480076268E /* VisibilityTracker.swift */; };
D71AD8FD2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */; };
D71AD8FE2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */; };
D71AD8FF2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */; };
D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */; };
D71AD9012CEC2398002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */; };
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; }; D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; };
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; }; D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
D723411A2B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; }; D723411A2B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
@@ -2266,7 +2270,6 @@
4CE6DEF727F7A08200C66700 /* damusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusTests.swift; sourceTree = "<group>"; }; 4CE6DEF727F7A08200C66700 /* damusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusTests.swift; sourceTree = "<group>"; };
4CE6DEFD27F7A08200C66700 /* damusUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = damusUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4CE6DEFD27F7A08200C66700 /* damusUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = damusUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; }; 4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; };
4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; };
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; }; 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; };
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; }; 4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; }; 4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
@@ -2397,6 +2400,7 @@
D7100C5B2B77016700C59298 /* IAPProductStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPProductStateView.swift; sourceTree = "<group>"; }; D7100C5B2B77016700C59298 /* IAPProductStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPProductStateView.swift; sourceTree = "<group>"; };
D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleStoreKitManager.swift; sourceTree = "<group>"; }; D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleStoreKitManager.swift; sourceTree = "<group>"; };
D71AC4CB2BA8E3480076268E /* VisibilityTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibilityTracker.swift; sourceTree = "<group>"; }; D71AC4CB2BA8E3480076268E /* VisibilityTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibilityTracker.swift; sourceTree = "<group>"; };
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccessibilityIdentifiers.swift; sourceTree = "<group>"; };
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; }; D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; };
D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleEnvironment.swift; sourceTree = "<group>"; }; D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleEnvironment.swift; sourceTree = "<group>"; };
D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; }; D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; };
@@ -3125,6 +3129,7 @@
643EA5C7296B764E005081BB /* RelayFilterView.swift */, 643EA5C7296B764E005081BB /* RelayFilterView.swift */,
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */, D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */, D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -3634,7 +3639,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4CE6DF0127F7A08200C66700 /* damusUITests.swift */, 4CE6DF0127F7A08200C66700 /* damusUITests.swift */,
4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */,
); );
path = damusUITests; path = damusUITests;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -4669,6 +4673,7 @@
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */, 5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */, 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
D71AD8FF2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */, 4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */, 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
@@ -4805,8 +4810,8 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D71AD9012CEC2398002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */, 4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */,
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -5165,6 +5170,7 @@
82D6FC062CD99F7900C925F4 /* ZapTypePicker.swift in Sources */, 82D6FC062CD99F7900C925F4 /* ZapTypePicker.swift in Sources */,
82D6FC072CD99F7900C925F4 /* ZapUserView.swift in Sources */, 82D6FC072CD99F7900C925F4 /* ZapUserView.swift in Sources */,
82D6FC082CD99F7900C925F4 /* ProfileZapLinkView.swift in Sources */, 82D6FC082CD99F7900C925F4 /* ProfileZapLinkView.swift in Sources */,
D71AD8FE2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
82D6FC092CD99F7900C925F4 /* AboutView.swift in Sources */, 82D6FC092CD99F7900C925F4 /* AboutView.swift in Sources */,
82D6FC0A2CD99F7900C925F4 /* ProfileName.swift in Sources */, 82D6FC0A2CD99F7900C925F4 /* ProfileName.swift in Sources */,
82D6FC0B2CD99F7900C925F4 /* ProfilePictureSelector.swift in Sources */, 82D6FC0B2CD99F7900C925F4 /* ProfilePictureSelector.swift in Sources */,
@@ -5443,6 +5449,7 @@
D73E5EB42C6A97F4007EB227 /* NoteContent.swift in Sources */, D73E5EB42C6A97F4007EB227 /* NoteContent.swift in Sources */,
D73E5EB52C6A97F4007EB227 /* LongformEvent.swift in Sources */, D73E5EB52C6A97F4007EB227 /* LongformEvent.swift in Sources */,
D73E5EB62C6A97F4007EB227 /* PushNotificationClient.swift in Sources */, D73E5EB62C6A97F4007EB227 /* PushNotificationClient.swift in Sources */,
D71AD8FD2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
D73E5EB72C6A97F4007EB227 /* HighlightEvent.swift in Sources */, D73E5EB72C6A97F4007EB227 /* HighlightEvent.swift in Sources */,
D73E5EB82C6A97F4007EB227 /* RelayConnection.swift in Sources */, D73E5EB82C6A97F4007EB227 /* RelayConnection.swift in Sources */,
D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */, D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */,
@@ -5771,6 +5778,7 @@
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */, 4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */, D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */, D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */, D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */,
D7CCFC192B058A3F00323D86 /* Block.swift in Sources */, D7CCFC192B058A3F00323D86 /* Block.swift in Sources */,
D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */, D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */,

View File

@@ -40,7 +40,7 @@
</BuildableReference> </BuildableReference>
</TestableReference> </TestableReference>
<TestableReference <TestableReference
skipped = "YES"> skipped = "NO">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700" BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"

View File

@@ -20,6 +20,7 @@ struct DamusBackground: View {
.resizable() .resizable()
.frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .center) .frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .center)
.ignoresSafeArea() .ignoresSafeArea()
.accessibilityHidden(true)
} }
} }

View File

@@ -239,14 +239,7 @@ struct ContentView: View {
MainContent(damus: damus) MainContent(damus: damus)
.toolbar() { .toolbar() {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button { TopbarSideMenuButton(damus_state: damus, isSideBarOpened: $isSideBarOpened)
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, disable_animation: damus_state!.settings.disable_animation)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
.disabled(isSideBarOpened)
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
@@ -792,6 +785,25 @@ struct ContentView: View {
} }
} }
struct TopbarSideMenuButton: View {
let damus_state: DamusState
@Binding var isSideBarOpened: Bool
var body: some View {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
.accessibilityHidden(true) // Knowing there is a profile picture here leads to no actionable outcome to VoiceOver users, so it is best not to show it
}
.accessibilityIdentifier(AppAccessibilityIdentifiers.main_side_menu_button.rawValue)
.accessibilityLabel(NSLocalizedString("Side menu", comment: "Accessibility label for the side menu button at the topbar"))
.disabled(isSideBarOpened)
}
}
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil), appDelegate: nil) ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil), appDelegate: nil)

View File

@@ -0,0 +1,66 @@
//
// AppAccessibilityIdentifiers.swift
// damus
//
// Created by Daniel DAquino on 2024-11-18.
//
import Foundation
/// A collection of app-wide identifier constants used to facilitate UI tests to find the element they are looking for.
///
/// ## Implementation notes
///
/// - This is not an exhaustive list. Add more identifiers as needed.
/// - Organize this by separating each category with `MARK` comment markers and a unique prefix, each category separated by 2 empty lines
enum AppAccessibilityIdentifiers: String {
// MARK: Login
// Prefix: `sign_in`
/// Sign in button at the very start of the app
case sign_in_option_button
/// A secure text entry field where the user can put their private key when logging in
case sign_in_nsec_key_entry_field
/// Button to sign in after entering private key
case sign_in_confirm_button
// MARK: Onboarding
// Prefix: `onboarding`
/// The skip button on the onboarding sheet
case onboarding_sheet_skip_button
// MARK: Post composer
// Prefix: `post_composer`
/// The cancel post button
case post_composer_cancel_button
// MARK: Main interface layout
// Prefix: `main`
/// Profile picture item on the top toolbar, used to open the side menu
case main_side_menu_button
// MARK: Side menu
// Prefix: `side_menu`
/// The profile option in the side menu
case side_menu_profile_button
// MARK: Items specific to the user's own profile
// Prefix: `own_profile`
/// The edit profile button
case own_profile_edit_button
/// The button to edit the banner image on the profile
case own_profile_banner_image_edit_button
/// The button to pick the new banner image from URL
case own_profile_banner_image_edit_from_url
}

View File

@@ -32,15 +32,21 @@ struct EditBannerImageView: View {
.onFailureImage(defaultImage) .onFailureImage(defaultImage)
.kfClickable() .kfClickable()
if #available(iOS 17.0, *) { EditPictureControl(uploader: damus_state.settings.default_media_uploader, keypair: damus_state.keypair, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback)
EditPictureControl(uploader: damus_state.settings.default_media_uploader, keypair: damus_state.keypair, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback) .padding(10)
.padding(10) .backwardsCompatibleSafeAreaPadding(self.safeAreaInsets)
.safeAreaPadding(self.safeAreaInsets) .accessibilityLabel(NSLocalizedString("Edit banner image", comment: "Accessibility label for edit banner image button"))
} else { .accessibilityIdentifier(AppAccessibilityIdentifiers.own_profile_banner_image_edit_button.rawValue)
EditPictureControl(uploader: damus_state.settings.default_media_uploader, keypair: damus_state.keypair, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback) }
.padding(10) }
.padding(.top, self.safeAreaInsets.top) }
}
extension View {
fileprivate func backwardsCompatibleSafeAreaPadding(_ insets: EdgeInsets) -> some View {
if #available(iOS 17.0, *) {
return self.safeAreaPadding(insets)
} else {
return self.padding(.top, insets.top)
} }
} }
} }

View File

@@ -104,6 +104,7 @@ struct LoginView: View {
} }
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
} }
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_in_confirm_button.rawValue)
.buttonStyle(GradientButtonStyle()) .buttonStyle(GradientButtonStyle())
.padding(.top, 10) .padding(.top, 10)
} }
@@ -299,27 +300,35 @@ struct KeyInput: View {
var body: some View { var body: some View {
HStack { HStack {
Image(systemName: "doc.on.clipboard") Button(action: {
.foregroundColor(.gray) if let pastedkey = UIPasteboard.general.string {
.onTapGesture { self.key.wrappedValue = pastedkey
if let pastedkey = UIPasteboard.general.string {
self.key.wrappedValue = pastedkey
}
} }
}, label: {
Image(systemName: "doc.on.clipboard")
})
.foregroundColor(.gray)
.accessibilityLabel(NSLocalizedString("Paste private key", comment: "Accessibility label for the private key paste button"))
SignInScan(shouldSaveKey: shouldSaveKey, loginKey: key, privKeyFound: privKeyFound) SignInScan(shouldSaveKey: shouldSaveKey, loginKey: key, privKeyFound: privKeyFound)
if is_secured { if is_secured {
SecureField("", text: key) SecureField("", text: key)
.nsecLoginStyle(key: key.wrappedValue, title: title) .nsecLoginStyle(key: key.wrappedValue, title: title)
} else { .accessibilityLabel(NSLocalizedString("Account private key", comment: "Accessibility label for the private key input field"))
TextField("", text: key) } else {
.nsecLoginStyle(key: key.wrappedValue, title: title) TextField("", text: key)
} .nsecLoginStyle(key: key.wrappedValue, title: title)
Image(systemName: "eye.slash") .accessibilityLabel(NSLocalizedString("Account private key", comment: "Accessibility label for the private key input field"))
.foregroundColor(.gray) }
.onTapGesture {
is_secured.toggle() Button(action: {
} is_secured.toggle()
}, label: {
Image(systemName: "eye.slash")
})
.foregroundColor(.gray)
.accessibilityLabel(NSLocalizedString("Toggle key visibility", comment: "Accessibility label for toggling the visibility of the private key input field"))
} }
.padding(.vertical, 2) .padding(.vertical, 2)
.padding(.horizontal, 10) .padding(.horizontal, 10)
@@ -342,6 +351,7 @@ struct SignInHeader: View {
.frame(width: 56, height: 56, alignment: .center) .frame(width: 56, height: 56, alignment: .center)
.shadow(color: DamusColors.purple, radius: 2) .shadow(color: DamusColors.purple, radius: 2)
.padding(.bottom) .padding(.bottom)
.accessibilityLabel(NSLocalizedString("Damus logo", comment: "Accessibility label for damus logo"))
Text("Sign in", comment: "Title of view to log into an account.") Text("Sign in", comment: "Title of view to log into an account.")
.foregroundColor(DamusColors.neutral6) .foregroundColor(DamusColors.neutral6)
@@ -365,10 +375,12 @@ struct SignInEntry: View {
.fontWeight(.medium) .fontWeight(.medium)
.padding(.top, 30) .padding(.top, 30)
KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), KeyInput(NSLocalizedString("nsec1", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."),
key: key, key: key,
shouldSaveKey: shouldSaveKey, shouldSaveKey: shouldSaveKey,
privKeyFound: $privKeyFound) privKeyFound: $privKeyFound)
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_in_nsec_key_entry_field.rawValue)
if privKeyFound { if privKeyFound {
Toggle(NSLocalizedString("Save Key in Secure Keychain", comment: "Toggle to save private key to the Apple secure keychain."), isOn: shouldSaveKey) Toggle(NSLocalizedString("Save Key in Secure Keychain", comment: "Toggle to save private key to the Apple secure keychain."), isOn: shouldSaveKey)
} }
@@ -389,7 +401,7 @@ struct SignInScan: View {
Button(action: { showQR.toggle() }, label: { Button(action: { showQR.toggle() }, label: {
Image(systemName: "qrcode.viewfinder")}) Image(systemName: "qrcode.viewfinder")})
.foregroundColor(.gray) .foregroundColor(.gray)
.accessibilityLabel(NSLocalizedString("Scan QR code", comment: "Accessibility label for a button that scans a private key QR code"))
} }
.sheet(isPresented: $showQR, onDismiss: { .sheet(isPresented: $showQR, onDismiss: {
if qrkey == nil { resetView() }} if qrkey == nil { resetView() }}

View File

@@ -38,7 +38,9 @@ struct OnboardingSuggestionsView: View {
}, label: { }, label: {
Text("Skip", comment: "Button to dismiss the suggested users screen") Text("Skip", comment: "Button to dismiss the suggested users screen")
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
})) })
.accessibilityIdentifier(AppAccessibilityIdentifiers.onboarding_sheet_skip_button.rawValue)
)
.tag(0) .tag(0)
PostView( PostView(

View File

@@ -304,6 +304,7 @@ struct PostView: View {
.padding(10) .padding(10)
}) })
.buttonStyle(NeutralButtonStyle()) .buttonStyle(NeutralButtonStyle())
.accessibilityIdentifier(AppAccessibilityIdentifiers.post_composer_cancel_button.rawValue)
if let error { if let error {
Text(error) Text(error)

View File

@@ -41,6 +41,7 @@ struct EditPictureControl: View {
}) { }) {
Text("Image URL", comment: "Option to enter a url") Text("Image URL", comment: "Option to enter a url")
} }
.accessibilityIdentifier(AppAccessibilityIdentifiers.own_profile_banner_image_edit_from_url.rawValue)
Button(action: { Button(action: {
self.show_library = true self.show_library = true

View File

@@ -27,6 +27,7 @@ struct ProfileEditButton: View {
.minimumScaleFactor(0.5) .minimumScaleFactor(0.5)
.lineLimit(1) .lineLimit(1)
} }
.accessibilityIdentifier(AppAccessibilityIdentifiers.own_profile_edit_button.rawValue)
} }
func fillColor() -> Color { func fillColor() -> Color {

View File

@@ -56,6 +56,7 @@ struct SetupView: View {
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
} }
.buttonStyle(GradientButtonStyle()) .buttonStyle(GradientButtonStyle())
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_in_option_button.rawValue)
.padding() .padding()
Button(action: { Button(action: {

View File

@@ -40,6 +40,7 @@ struct SideMenuView: View {
NavigationLink(value: Route.Profile(profile: profile_model, followers: followers)) { NavigationLink(value: Route.Profile(profile: profile_model, followers: followers)) {
navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), img: "user") navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), img: "user")
} }
.accessibilityIdentifier(AppAccessibilityIdentifiers.side_menu_profile_button.rawValue)
NavigationLink(value: Route.Wallet(wallet: damus_state.wallet)) { NavigationLink(value: Route.Wallet(wallet: damus_state.wallet)) {
navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet") navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet")

View File

@@ -49,14 +49,7 @@ struct PostingTimelineView: View {
.frame(height: getSafeAreaTop()) .frame(height: getSafeAreaTop())
HStack(alignment: .top) { HStack(alignment: .top) {
Button { TopbarSideMenuButton(damus_state: damus_state, isSideBarOpened: $isSideBarOpened)
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
.disabled(isSideBarOpened)
Spacer() Spacer()

View File

@@ -8,34 +8,85 @@
import XCTest import XCTest
class damusUITests: XCTestCase { class damusUITests: XCTestCase {
var app = XCUIApplication()
typealias AID = AppAccessibilityIdentifiers
override func setUpWithError() throws { override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class. // Put setup code here. This method is called before the invocation of each test method in the class.
self.app = XCUIApplication()
// In UI tests it is usually best to stop immediately when a failure occurs. // In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. // In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
// Set app language to English
app.launchArguments += ["-AppleLanguages", "(en)"]
app.launchArguments += ["-AppleLocale", "en_US"]
// Force portrait orientation
XCUIDevice.shared.orientation = .portrait
// Optional: Reset the device's orientation before each test
addTeardownBlock {
XCUIDevice.shared.orientation = .portrait
}
app.launch()
} }
override func tearDownWithError() throws { override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
func testExample() throws { /// Tests if banner edit button is clickable.
// UI tests must launch the application that they test. /// Note: This is able to detect if the button is obscured by an invisible overlaying object.
let app = XCUIApplication() /// See https://github.com/damus-io/damus/issues/2636 for the kind of issue this guards against.
app.launch() func testEditBannerImage() throws {
// Use XCTAssert and related functions to verify your tests produce the correct results. // Use XCTAssert and related functions to verify your tests produce the correct results.
try self.loginIfNotAlready()
guard app.buttons[AID.main_side_menu_button.rawValue].tapIfExists(timeout: 5) else { throw DamusUITestError.timeout_waiting_for_element }
guard app.buttons[AID.side_menu_profile_button.rawValue].tapIfExists(timeout: 5) else { throw DamusUITestError.timeout_waiting_for_element }
guard app.buttons[AID.own_profile_edit_button.rawValue].tapIfExists(timeout: 5) else { throw DamusUITestError.timeout_waiting_for_element }
guard app.buttons[AID.own_profile_banner_image_edit_button.rawValue].waitForExistence(timeout: 5) else { throw DamusUITestError.timeout_waiting_for_element }
let bannerEditButtonCoordinates = app.buttons[AID.own_profile_banner_image_edit_button.rawValue].coordinate(withNormalizedOffset: CGVector.zero).withOffset(CGVector(dx: 15, dy: 15))
bannerEditButtonCoordinates.tap()
guard app.buttons[AID.own_profile_banner_image_edit_from_url.rawValue].waitForExistence(timeout: 5) else { throw DamusUITestError.timeout_waiting_for_element }
} }
func testLaunchPerformance() throws { func loginIfNotAlready() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { if app.buttons[AID.sign_in_option_button.rawValue].waitForExistence(timeout: 5) {
// This measures how long it takes to launch your application. try self.login()
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
} }
app.buttons[AID.onboarding_sheet_skip_button.rawValue].tapIfExists(timeout: 5)
app.buttons[AID.post_composer_cancel_button.rawValue].tapIfExists(timeout: 5)
}
func login() throws {
app.buttons[AID.sign_in_option_button.rawValue].tap()
guard app.secureTextFields[AID.sign_in_nsec_key_entry_field.rawValue].tapIfExists(timeout: 10) else { throw DamusUITestError.timeout_waiting_for_element }
app.typeText("nsec1vxvz8c7070d99njn0aqpcttljnzhfutt422l0r37yep7htesd0mq9p8fg2")
guard app.buttons[AID.sign_in_confirm_button.rawValue].tapIfExists(timeout: 5) else { throw DamusUITestError.timeout_waiting_for_element }
}
enum DamusUITestError: Error {
case timeout_waiting_for_element
}
}
extension XCUIElement {
@discardableResult
func tapIfExists(timeout: TimeInterval) -> Bool {
if self.waitForExistence(timeout: timeout) {
self.tap()
return true
}
return false
} }
} }

View File

@@ -1,32 +0,0 @@
//
// damusUITestsLaunchTests.swift
// damusUITests
//
// Created by William Casarin on 2022-04-01.
//
import XCTest
class damusUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}