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:
@@ -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 */,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "YES">
|
skipped = "NO">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
|
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
66
damus/Views/AppAccessibilityIdentifiers.swift
Normal file
66
damus/Views/AppAccessibilityIdentifiers.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 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.
|
// 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 {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user