commit 5c6e5ca2de73e6152cc662306abd72d836cf5b24 parent e3105a90c58bc0ea6d49565312aae796c17f7b3b Author: Daniel D’Aquino <daniel@daquino.me> Date: Sun, 17 Nov 2024 13:13:52 -0800 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> Diffstat:
16 files changed, 217 insertions(+), 93 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -338,7 +338,6 @@ 4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CE6DEED27F7A08200C66700 /* Preview Assets.xcassets */; }; 4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEF727F7A08200C66700 /* damusTests.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 */; }; 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.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 */; }; D7100C5E2B7709ED00C59298 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.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 */; }; D72341192B6864F200E1E135 /* 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>"; }; 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>"; }; - 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>"; }; 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>"; }; @@ -2397,6 +2400,7 @@ 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>"; }; 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>"; }; 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>"; }; @@ -3125,6 +3129,7 @@ 643EA5C7296B764E005081BB /* RelayFilterView.swift */, D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */, D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */, + D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */, ); path = Views; sourceTree = "<group>"; @@ -3634,7 +3639,6 @@ isa = PBXGroup; children = ( 4CE6DF0127F7A08200C66700 /* damusUITests.swift */, - 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */, ); path = damusUITests; sourceTree = "<group>"; @@ -4669,6 +4673,7 @@ 5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */, 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, + D71AD8FF2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */, 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */, @@ -4805,8 +4810,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D71AD9012CEC2398002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, 4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */, - 4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5165,6 +5170,7 @@ 82D6FC062CD99F7900C925F4 /* ZapTypePicker.swift in Sources */, 82D6FC072CD99F7900C925F4 /* ZapUserView.swift in Sources */, 82D6FC082CD99F7900C925F4 /* ProfileZapLinkView.swift in Sources */, + D71AD8FE2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, 82D6FC092CD99F7900C925F4 /* AboutView.swift in Sources */, 82D6FC0A2CD99F7900C925F4 /* ProfileName.swift in Sources */, 82D6FC0B2CD99F7900C925F4 /* ProfilePictureSelector.swift in Sources */, @@ -5443,6 +5449,7 @@ D73E5EB42C6A97F4007EB227 /* NoteContent.swift in Sources */, D73E5EB52C6A97F4007EB227 /* LongformEvent.swift in Sources */, D73E5EB62C6A97F4007EB227 /* PushNotificationClient.swift in Sources */, + D71AD8FD2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, D73E5EB72C6A97F4007EB227 /* HighlightEvent.swift in Sources */, D73E5EB82C6A97F4007EB227 /* RelayConnection.swift in Sources */, D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */, @@ -5771,6 +5778,7 @@ 4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */, D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */, D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */, + D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */, D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */, D7CCFC192B058A3F00323D86 /* Block.swift in Sources */, D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */, diff --git a/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme b/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme @@ -40,7 +40,7 @@ </BuildableReference> </TestableReference> <TestableReference - skipped = "YES"> + skipped = "NO"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "4CE6DEFC27F7A08200C66700" diff --git a/damus/Components/Gradients/DamusBackground.swift b/damus/Components/Gradients/DamusBackground.swift @@ -20,6 +20,7 @@ struct DamusBackground: View { .resizable() .frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .center) .ignoresSafeArea() + .accessibilityHidden(true) } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -239,14 +239,7 @@ struct ContentView: View { MainContent(damus: damus) .toolbar() { ToolbarItem(placement: .navigationBarLeading) { - 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) - } - .disabled(isSideBarOpened) + TopbarSideMenuButton(damus_state: damus, isSideBarOpened: $isSideBarOpened) } 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 { static var previews: some View { ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil), appDelegate: nil) diff --git a/damus/Views/AppAccessibilityIdentifiers.swift b/damus/Views/AppAccessibilityIdentifiers.swift @@ -0,0 +1,66 @@ +// +// AppAccessibilityIdentifiers.swift +// damus +// +// Created by Daniel D’Aquino 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 +} diff --git a/damus/Views/BannerImageView.swift b/damus/Views/BannerImageView.swift @@ -32,15 +32,21 @@ struct EditBannerImageView: View { .onFailureImage(defaultImage) .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) - .padding(10) - .safeAreaPadding(self.safeAreaInsets) - } else { - 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) - } + 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) + .backwardsCompatibleSafeAreaPadding(self.safeAreaInsets) + .accessibilityLabel(NSLocalizedString("Edit banner image", comment: "Accessibility label for edit banner image button")) + .accessibilityIdentifier(AppAccessibilityIdentifiers.own_profile_banner_image_edit_button.rawValue) + } + } +} + +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) } } } diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift @@ -104,6 +104,7 @@ struct LoginView: View { } .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) } + .accessibilityIdentifier(AppAccessibilityIdentifiers.sign_in_confirm_button.rawValue) .buttonStyle(GradientButtonStyle()) .padding(.top, 10) } @@ -299,27 +300,35 @@ struct KeyInput: View { var body: some View { HStack { - Image(systemName: "doc.on.clipboard") - .foregroundColor(.gray) - .onTapGesture { - if let pastedkey = UIPasteboard.general.string { - self.key.wrappedValue = pastedkey - } + Button(action: { + 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) if is_secured { - SecureField("", text: key) - .nsecLoginStyle(key: key.wrappedValue, title: title) - } else { - TextField("", text: key) - .nsecLoginStyle(key: key.wrappedValue, title: title) - } - Image(systemName: "eye.slash") - .foregroundColor(.gray) - .onTapGesture { - is_secured.toggle() - } + SecureField("", text: key) + .nsecLoginStyle(key: key.wrappedValue, title: title) + .accessibilityLabel(NSLocalizedString("Account private key", comment: "Accessibility label for the private key input field")) + } else { + TextField("", text: key) + .nsecLoginStyle(key: key.wrappedValue, title: title) + .accessibilityLabel(NSLocalizedString("Account private key", comment: "Accessibility label for the private key input field")) + } + + 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(.horizontal, 10) @@ -342,6 +351,7 @@ struct SignInHeader: View { .frame(width: 56, height: 56, alignment: .center) .shadow(color: DamusColors.purple, radius: 2) .padding(.bottom) + .accessibilityLabel(NSLocalizedString("Damus logo", comment: "Accessibility label for damus logo")) Text("Sign in", comment: "Title of view to log into an account.") .foregroundColor(DamusColors.neutral6) @@ -365,10 +375,12 @@ struct SignInEntry: View { .fontWeight(.medium) .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, shouldSaveKey: shouldSaveKey, privKeyFound: $privKeyFound) + .accessibilityIdentifier(AppAccessibilityIdentifiers.sign_in_nsec_key_entry_field.rawValue) + if privKeyFound { 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: { Image(systemName: "qrcode.viewfinder")}) .foregroundColor(.gray) - + .accessibilityLabel(NSLocalizedString("Scan QR code", comment: "Accessibility label for a button that scans a private key QR code")) } .sheet(isPresented: $showQR, onDismiss: { if qrkey == nil { resetView() }} diff --git a/damus/Views/Onboarding/OnboardingSuggestionsView.swift b/damus/Views/Onboarding/OnboardingSuggestionsView.swift @@ -38,7 +38,9 @@ struct OnboardingSuggestionsView: View { }, label: { Text("Skip", comment: "Button to dismiss the suggested users screen") .font(.subheadline.weight(.semibold)) - })) + }) + .accessibilityIdentifier(AppAccessibilityIdentifiers.onboarding_sheet_skip_button.rawValue) + ) .tag(0) PostView( diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -304,6 +304,7 @@ struct PostView: View { .padding(10) }) .buttonStyle(NeutralButtonStyle()) + .accessibilityIdentifier(AppAccessibilityIdentifiers.post_composer_cancel_button.rawValue) if let error { Text(error) diff --git a/damus/Views/Profile/EditPictureControl.swift b/damus/Views/Profile/EditPictureControl.swift @@ -41,6 +41,7 @@ struct EditPictureControl: View { }) { Text("Image URL", comment: "Option to enter a url") } + .accessibilityIdentifier(AppAccessibilityIdentifiers.own_profile_banner_image_edit_from_url.rawValue) Button(action: { self.show_library = true diff --git a/damus/Views/Profile/ProfileEditButton.swift b/damus/Views/Profile/ProfileEditButton.swift @@ -27,6 +27,7 @@ struct ProfileEditButton: View { .minimumScaleFactor(0.5) .lineLimit(1) } + .accessibilityIdentifier(AppAccessibilityIdentifiers.own_profile_edit_button.rawValue) } func fillColor() -> Color { diff --git a/damus/Views/SetupView.swift b/damus/Views/SetupView.swift @@ -56,6 +56,7 @@ struct SetupView: View { .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) } .buttonStyle(GradientButtonStyle()) + .accessibilityIdentifier(AppAccessibilityIdentifiers.sign_in_option_button.rawValue) .padding() Button(action: { diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift @@ -40,6 +40,7 @@ struct SideMenuView: View { NavigationLink(value: Route.Profile(profile: profile_model, followers: followers)) { 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)) { navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet") diff --git a/damus/Views/Timeline/PostingTimelineView.swift b/damus/Views/Timeline/PostingTimelineView.swift @@ -49,14 +49,7 @@ struct PostingTimelineView: View { .frame(height: getSafeAreaTop()) HStack(alignment: .top) { - 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) - } - .disabled(isSideBarOpened) + TopbarSideMenuButton(damus_state: damus_state, isSideBarOpened: $isSideBarOpened) Spacer() diff --git a/damusUITests/damusUITests.swift b/damusUITests/damusUITests.swift @@ -8,34 +8,85 @@ import XCTest class damusUITests: XCTestCase { + var app = XCUIApplication() + typealias AID = AppAccessibilityIdentifiers override func setUpWithError() throws { // 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. continueAfterFailure = false // In UI tests it’s 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 { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - + /// Tests if banner edit button is clickable. + /// Note: This is able to detect if the button is obscured by an invisible overlaying object. + /// See https://github.com/damus-io/damus/issues/2636 for the kind of issue this guards against. + func testEditBannerImage() throws { // 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 loginIfNotAlready() throws { + if app.buttons[AID.sign_in_option_button.rawValue].waitForExistence(timeout: 5) { + try self.login() + } + + 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 + } +} - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } +extension XCUIElement { + @discardableResult + func tapIfExists(timeout: TimeInterval) -> Bool { + if self.waitForExistence(timeout: timeout) { + self.tap() + return true } + return false } } diff --git a/damusUITests/damusUITestsLaunchTests.swift b/damusUITests/damusUITestsLaunchTests.swift @@ -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) - } -}