diff --git a/Flinky.xcodeproj/project.pbxproj b/Flinky.xcodeproj/project.pbxproj
index eed6623..81f3ceb 100644
--- a/Flinky.xcodeproj/project.pbxproj
+++ b/Flinky.xcodeproj/project.pbxproj
@@ -1859,8 +1859,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/getsentry/sentry-cocoa";
requirement = {
- kind = exactVersion;
- version = 9.15.0;
+ branch = "feat/feedback-presentation-api";
+ kind = branch;
};
traits = (
);
diff --git a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index e358642..2d7354e 100644
--- a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
- "revision" : "cef29e94feb00b1b712514443d6d70b09ef20355",
- "version" : "9.15.0"
+ "branch" : "feat/feedback-presentation-api",
+ "revision" : "3fa7efdac738a631246f4c0ccf7f8217762ba9ef"
}
},
{
diff --git a/Targets/App/Sources/Environment/Environment+Feedback.swift b/Targets/App/Sources/Environment/Environment+Feedback.swift
new file mode 100644
index 0000000..4e98523
--- /dev/null
+++ b/Targets/App/Sources/Environment/Environment+Feedback.swift
@@ -0,0 +1,24 @@
+import SwiftUI
+
+struct FeedbackAction {
+ private let handler: () -> Void
+
+ init(handler: @escaping () -> Void = {}) {
+ self.handler = handler
+ }
+
+ func show() {
+ handler()
+ }
+}
+
+extension EnvironmentValues {
+ var feedback: FeedbackAction {
+ get { self[FeedbackKey.self] }
+ set { self[FeedbackKey.self] = newValue }
+ }
+
+ private struct FeedbackKey: EnvironmentKey {
+ static let defaultValue = FeedbackAction()
+ }
+}
diff --git a/Targets/App/Sources/Generated/L10n.swift b/Targets/App/Sources/Generated/L10n.swift
index 63132c4..b752d78 100644
--- a/Targets/App/Sources/Generated/L10n.swift
+++ b/Targets/App/Sources/Generated/L10n.swift
@@ -39,6 +39,16 @@ internal enum L10n {
/// New List
internal static let title = L10n.tr("create-list.title", fallback: "New List")
}
+ internal enum Feedback {
+ internal enum Toast {
+ internal enum Submit {
+ /// Failed to submit feedback
+ internal static let error = L10n.tr("feedback.toast.submit.error", fallback: "Failed to submit feedback")
+ /// Feedback submitted successfully
+ internal static let success = L10n.tr("feedback.toast.submit.success", fallback: "Feedback submitted successfully")
+ }
+ }
+ }
internal enum LinkDetail {
internal enum EditLink {
/// Edit
@@ -217,6 +227,16 @@ internal enum L10n {
internal static let label = L10n.tr("shared.button.edit.accessibility.label", fallback: "Edit")
}
}
+ internal enum Feedback {
+ /// Send Feedback
+ internal static let label = L10n.tr("shared.button.feedback.label", fallback: "Send Feedback")
+ internal enum Accessibility {
+ /// Send feedback to help improve the app
+ internal static let hint = L10n.tr("shared.button.feedback.accessibility.hint", fallback: "Send feedback to help improve the app")
+ /// Send Feedback
+ internal static let label = L10n.tr("shared.button.feedback.accessibility.label", fallback: "Send Feedback")
+ }
+ }
internal enum NewLink {
internal enum Accessibility {
/// Create a new link in this list
diff --git a/Targets/App/Sources/Main/FlinkyApp.swift b/Targets/App/Sources/Main/FlinkyApp.swift
index 04966cf..88994e9 100644
--- a/Targets/App/Sources/Main/FlinkyApp.swift
+++ b/Targets/App/Sources/Main/FlinkyApp.swift
@@ -13,15 +13,10 @@ struct FlinkyApp: App {
private let sharedModelContainer: ModelContainer
init() {
- SentrySDK.start { options in
- Self.configureSentry(options: options)
+ SentrySDK.start { [_toastManager] options in
+ Self.configureSentry(options: options, toastManager: { _toastManager.wrappedValue })
}
- // Start app health observation for system-level metrics
- // (thermal state, network reachability, app state transitions)
- // Reference: https://github.com/getsentry/sentry-cocoa/issues/7000
- AppHealthObserver.shared.startObserving()
-
do {
sharedModelContainer = try SharedModelContainerFactory.make(
isStoredInMemoryOnly: ProcessInfo.processInfo.isTestingEnabled
@@ -30,6 +25,11 @@ struct FlinkyApp: App {
fatalError("Failed to create shared ModelContainer: \(error)")
}
+ // Start app health observation for system-level metrics
+ // (thermal state, network reachability, app state transitions)
+ // Reference: https://github.com/getsentry/sentry-cocoa/issues/7000
+ AppHealthObserver.shared.startObserving()
+
// Seed if needed on first app launch
DataSeedingService.seedDataIfNeeded(modelContext: sharedModelContainer.mainContext)
}
@@ -39,7 +39,11 @@ struct FlinkyApp: App {
/// This method is defined as `private static` to because it is called from a non-mutating context.
///
/// - Parameter options: Options structure to configure Sentry.
- private static func configureSentry(options: Options) { // swiftlint:disable:this function_body_length
+ /// - Parameter toastManager: Closure providing lazy access to the toast manager for feedback callbacks.
+ private static func configureSentry( // swiftlint:disable:this function_body_length
+ options: Options,
+ toastManager: @escaping () -> ToastManager
+ ) {
// Disable Sentry for tests because it produces a lot of noise.
if ProcessInfo.processInfo.isTestingEnabled {
Self.logger.warning("Sentry is disabled in test environment")
@@ -117,15 +121,6 @@ struct FlinkyApp: App {
// Configure User Feedback
options.configureUserFeedback = { feedbackOptions in
feedbackOptions.animations = true
- feedbackOptions.configureWidget = { widgetOptions in
- widgetOptions.autoInject = false // Disable automatic injection of the widget, because it's not supported in SwiftUI.
- widgetOptions.labelText = "Send Feedback"
- widgetOptions.showIcon = true
- widgetOptions.widgetAccessibilityLabel = "Feedback Widget"
- widgetOptions.windowLevel = UIWindow.Level.normal + 1
- widgetOptions.location = [.bottom, .trailing]
- widgetOptions.layoutUIOffset = .init(horizontal: 18, vertical: 80)
- }
feedbackOptions.useShakeGesture = true
feedbackOptions.showFormForScreenshots = true
feedbackOptions.configureForm = { formOptions in
@@ -203,6 +198,24 @@ struct FlinkyApp: App {
// Track feedback form closing using metrics - better for aggregate counts than individual events
SentryMetricsHelper.trackFeedbackFormClosed()
}
+ feedbackOptions.onSubmitSuccess = { _ in
+ let breadcrumb = Breadcrumb(level: .info, category: "user_feedback")
+ breadcrumb.message = "User successfully submitted feedback"
+ SentrySDK.addBreadcrumb(breadcrumb)
+
+ DispatchQueue.main.async {
+ toastManager().success(description: L10n.Feedback.Toast.Submit.success)
+ }
+ }
+ feedbackOptions.onSubmitError = { error in
+ let breadcrumb = Breadcrumb(level: .error, category: "user_feedback")
+ breadcrumb.message = "Feedback submission failed: \(error.localizedDescription)"
+ SentrySDK.addBreadcrumb(breadcrumb)
+
+ DispatchQueue.main.async {
+ toastManager().error(description: L10n.Feedback.Toast.Submit.error)
+ }
+ }
}
// Configure Logs
@@ -222,6 +235,9 @@ struct FlinkyApp: App {
var body: some Scene {
WindowGroup {
MainContainerView()
+ .environment(\.feedback, FeedbackAction {
+ SentrySDK.feedback.show()
+ })
.toaster(toastManager)
.configureOnLaunch { options in
options.publicKey = "30d2f7cc2fa469eaf8e4bdf958ad9d66bce491a7da1fb08ff0a7156a8e15a47d"
diff --git a/Targets/App/Sources/Resources/Localizable.xcstrings b/Targets/App/Sources/Resources/Localizable.xcstrings
index c89ff60..9961efe 100644
--- a/Targets/App/Sources/Resources/Localizable.xcstrings
+++ b/Targets/App/Sources/Resources/Localizable.xcstrings
@@ -795,6 +795,42 @@
}
}
},
+ "shared.button.feedback.accessibility.hint": {
+ "comment": "Feedback button accessibility hint",
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Send feedback to help improve the app"
+ }
+ }
+ }
+ },
+ "shared.button.feedback.accessibility.label": {
+ "comment": "Feedback button accessibility label",
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Send Feedback"
+ }
+ }
+ }
+ },
+ "shared.button.feedback.label": {
+ "comment": "Feedback button text",
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Send Feedback"
+ }
+ }
+ }
+ },
"shared.color-picker.accessibility.label": {
"comment": "Color picker accessibility label",
"extractionState": "manual",
@@ -1466,6 +1502,30 @@
}
}
}
+ },
+ "feedback.toast.submit.success": {
+ "comment": "Toast message shown when feedback is submitted successfully",
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Feedback submitted successfully"
+ }
+ }
+ }
+ },
+ "feedback.toast.submit.error": {
+ "comment": "Toast message shown when feedback submission fails",
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Failed to submit feedback"
+ }
+ }
+ }
}
},
"version": "1.0"
diff --git a/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt b/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt
index 8032d35..a7887ec 100644
--- a/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt
+++ b/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt
@@ -1,12 +1,12 @@
name: OnLaunch-iOS-Client, nameSpecified: OnLaunch-iOS-Client, owner: kula-app, version: 0.0.6, source: https://github.com/kula-app/OnLaunch-iOS-Client
-name: sentry-cocoa, nameSpecified: Sentry, owner: getsentry, version: 9.15.0, source: https://github.com/getsentry/sentry-cocoa
+name: sentry-cocoa, nameSpecified: sentry-cocoa, owner: getsentry, version: , source: https://github.com/getsentry/sentry-cocoa
name: SFSafeSymbols, nameSpecified: SFSafeSymbols, owner: SFSafeSymbols, version: 7.0.0, source: https://github.com/SFSafeSymbols/SFSafeSymbols
name: OnLaunch-iOS-Client, nameSpecified: OnLaunch-iOS-Client, owner: kula-app, version: 0.0.6, source: https://github.com/kula-app/OnLaunch-iOS-Client
-name: sentry-cocoa, nameSpecified: Sentry, owner: getsentry, version: 9.15.0, source: https://github.com/getsentry/sentry-cocoa
+name: sentry-cocoa, nameSpecified: sentry-cocoa, owner: getsentry, version: , source: https://github.com/getsentry/sentry-cocoa
name: SFSafeSymbols, nameSpecified: SFSafeSymbols, owner: SFSafeSymbols, version: 7.0.0, source: https://github.com/SFSafeSymbols/SFSafeSymbols
diff --git a/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist b/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist
index 1829a0e..ffdd7ff 100644
--- a/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist
+++ b/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist
@@ -22,7 +22,7 @@
File
Licenses/sentry-cocoa
Title
- Sentry (9.15.0)
+ sentry-cocoa
Type
PSChildPaneSpecifier
diff --git a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift
index bd8ccba..44c055e 100644
--- a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift
+++ b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift
@@ -1,3 +1,4 @@
+import SFSafeSymbols
import SwiftUI
struct CreateLinkEditorRenderView: View {
@@ -7,6 +8,7 @@ struct CreateLinkEditorRenderView: View {
}
@Environment(\.dismiss) private var dismiss
+ @Environment(\.feedback) private var feedback
@Binding var name: String
@Binding var url: String
@@ -55,6 +57,16 @@ struct CreateLinkEditorRenderView: View {
.accessibilityHint(L10n.Shared.Button.Cancel.Accessibility.hint)
.accessibilityIdentifier("create-link.cancel.button")
}
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(action: {
+ feedback.show()
+ }, label: {
+ Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone)
+ })
+ .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label)
+ .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint)
+ .accessibilityIdentifier("create-link.feedback.button")
+ }
ToolbarItem(placement: .confirmationAction) {
if #available(iOS 26, *) {
Button(role: .confirm) {
diff --git a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift
index 20b0eff..82e46bc 100644
--- a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift
+++ b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift
@@ -1,3 +1,4 @@
+import SFSafeSymbols
import SwiftUI
struct CreateLinkListEditorRenderView: View {
@@ -6,6 +7,7 @@ struct CreateLinkListEditorRenderView: View {
}
@Environment(\.dismiss) private var dismiss
+ @Environment(\.feedback) private var feedback
@Binding var name: String
@FocusState private var focusedField: CreateField?
@@ -27,6 +29,16 @@ struct CreateLinkListEditorRenderView: View {
.navigationTitle(L10n.CreateList.title)
.accessibilityIdentifier("create-link-list.container")
.toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(action: {
+ feedback.show()
+ }, label: {
+ Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone)
+ })
+ .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label)
+ .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint)
+ .accessibilityIdentifier("create-link-list.feedback.button")
+ }
ToolbarItem(placement: .cancellationAction) {
Button(role: .cancel) {
dismiss()
diff --git a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift
index cedb8b5..d6d63b6 100644
--- a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift
+++ b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift
@@ -7,6 +7,7 @@ import SwiftUI
struct LinkDetailRenderView: View {
@Environment(\.dismiss) private var dismiss
+ @Environment(\.feedback) private var feedback
let linkId: UUID
@@ -30,6 +31,16 @@ struct LinkDetailRenderView: View {
}
.background(Color(UIColor.systemGroupedBackground))
.toolbar {
+ ToolbarItemGroup(placement: .topBarLeading) {
+ Button(action: {
+ feedback.show()
+ }, label: {
+ Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone)
+ })
+ .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label)
+ .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint)
+ .accessibilityIdentifier("link-detail.feedback.button")
+ }
ToolbarItemGroup(placement: .topBarLeading) {
Menu {
Button {
diff --git a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift
index 75faa90..4e1475a 100644
--- a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift
+++ b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift
@@ -3,6 +3,8 @@ import SFSafeSymbols
import SwiftUI
struct LinkInfoRenderView: View {
+ @Environment(\.feedback) private var feedback
+
@Binding var name: String
@Binding var url: String
@Binding var color: ListColor
@@ -34,6 +36,16 @@ struct LinkInfoRenderView: View {
.accessibilityHint(L10n.Shared.Button.Cancel.Accessibility.hint)
.accessibilityIdentifier("link-info.cancel.button")
}
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(action: {
+ feedback.show()
+ }, label: {
+ Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone)
+ })
+ .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label)
+ .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint)
+ .accessibilityIdentifier("link-info.feedback.button")
+ }
if #available(iOS 26, *) {
ToolbarItem(placement: .confirmationAction) {
Button(role: .confirm) {
@@ -171,8 +183,8 @@ extension LinkInfoRenderView {
.font(.system(size: 22, weight: .medium))
.foregroundStyle(
symbol.isEmoji
- ? Color.blue
- : (colorScheme == .light ? Color.gray.mix(with: Color.black, by: 0.3) : Color.gray)
+ ? Color.blue
+ : (colorScheme == .light ? Color.gray.mix(with: Color.black, by: 0.3) : Color.gray)
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(6)
@@ -180,7 +192,7 @@ extension LinkInfoRenderView {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
symbol.isEmoji
- ? Color.blue.opacity(0.15) : Color.gray.opacity(colorScheme == .light ? 0.1 : 0.2)
+ ? Color.blue.opacity(0.15) : Color.gray.opacity(colorScheme == .light ? 0.1 : 0.2)
)
.clipShape(Circle())
.contentShape(Circle())
diff --git a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift
index 3c8bcb5..16d2ffb 100644
--- a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift
+++ b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift
@@ -3,6 +3,8 @@ import SFSafeSymbols
import SwiftUI
struct LinkListDetailRenderView: View {
+ @Environment(\.feedback) private var feedback
+
let list: LinkListsDisplayItem
let links: [LinkListDetailDisplayItem]
@@ -30,6 +32,16 @@ struct LinkListDetailRenderView: View {
emptyStateView
}
.toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(action: {
+ feedback.show()
+ }, label: {
+ Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone)
+ })
+ .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label)
+ .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint)
+ .accessibilityIdentifier("link-list-detail.feedback.button")
+ }
ToolbarItem(placement: .navigationBarTrailing) {
sortMenu
}
diff --git a/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift b/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift
index aa6e67f..77aeb00 100644
--- a/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift
+++ b/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift
@@ -194,11 +194,6 @@ struct LinkListsContainerView: View {
}
}
.sentryTrace("LINK_LISTS_VIEW")
- .onAppear {
- // Auto-injecting Sentry feedback widget is currently not supported in SwiftUI.
- // Therefore we manually trigger it when the view appears.
- SentrySDK.feedback.showWidget()
- }
.onChange(of: searchText) { oldValue, newValue in
// Track search when user starts searching (transitions from empty to non-empty)
if oldValue.isEmpty && !newValue.isEmpty {
diff --git a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift
index fed4c47..b349365 100644
--- a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift
+++ b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift
@@ -2,6 +2,8 @@ import SFSafeSymbols
import SwiftUI
struct LinkListsRenderView: View {
+ @Environment(\.feedback) private var feedback
+
let pinnedLists: [LinkListsDisplayItem]
let unpinnedLists: [LinkListsDisplayItem]
@@ -71,6 +73,16 @@ struct LinkListsRenderView: View {
}
.ifAvailable(.iOS26, modify: { view in
view.toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(action: {
+ feedback.show()
+ }, label: {
+ Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone)
+ })
+ .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label)
+ .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint)
+ .accessibilityIdentifier("link-lists.feedback.button")
+ }
if !pinnedLists.isEmpty || !unpinnedLists.isEmpty {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
@@ -111,6 +123,16 @@ struct LinkListsRenderView: View {
}
}, elseModify: { view in
view.toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(action: {
+ feedback.show()
+ }, label: {
+ Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone)
+ })
+ .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label)
+ .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint)
+ .accessibilityIdentifier("link-lists.feedback.button")
+ }
if !pinnedLists.isEmpty || !unpinnedLists.isEmpty {
ToolbarItem(placement: .topBarTrailing) {
EditButton()
diff --git a/Targets/UITests/Sources/UITests.swift b/Targets/UITests/Sources/UITests.swift
index dcca258..fb3a12b 100644
--- a/Targets/UITests/Sources/UITests.swift
+++ b/Targets/UITests/Sources/UITests.swift
@@ -2,15 +2,143 @@ import XCTest
final class UITests: XCTestCase {
+ private var app: XCUIApplication!
+
override func setUpWithError() throws {
- // In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
+ app = XCUIApplication()
+ app.launchEnvironment["TESTING"] = "1"
+ app.launch()
}
@MainActor
func testLaunchPerformance() throws {
measure(metrics: [XCTApplicationLaunchMetric()]) {
- XCUIApplication().launch()
+ let app = XCUIApplication()
+ app.launchEnvironment["TESTING"] = "1"
+ app.launch()
}
}
+
+ // MARK: - Feedback Button Tests
+
+ func testFeedbackButtonExistsOnListsView() throws {
+ // -- Act --
+ let feedbackButton = app.buttons["link-lists.feedback.button"]
+ XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5))
+ XCTAssertTrue(feedbackButton.isHittable)
+ feedbackButton.tap()
+
+ // -- Assert --
+ assertFeedbackSubmitButtonExists()
+ }
+
+ func testFeedbackButtonExistsOnListDetailView() throws {
+ // -- Arrange --
+ navigateToListDetail()
+
+ // -- Act --
+ let feedbackButton = app.buttons["link-list-detail.feedback.button"]
+ XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5))
+ XCTAssertTrue(feedbackButton.isHittable)
+ feedbackButton.tap()
+
+ // -- Assert --
+ assertFeedbackSubmitButtonExists()
+ }
+
+ func testFeedbackButtonExistsOnLinkDetailView() throws {
+ // -- Arrange --
+ navigateToLinkDetail()
+
+ // -- Act --
+ let feedbackButton = app.buttons["link-detail.feedback.button"]
+ XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5))
+ XCTAssertTrue(feedbackButton.isHittable)
+ feedbackButton.tap()
+
+ // -- Assert --
+ assertFeedbackSubmitButtonExists()
+ }
+
+ func testFeedbackButtonExistsOnLinkInfoView() throws {
+ // -- Arrange --
+ navigateToLinkDetail()
+
+ let moreMenu = app.buttons["link-detail.more-menu.button"]
+ XCTAssertTrue(moreMenu.waitForExistence(timeout: 5))
+ moreMenu.tap()
+
+ let editButton = app.buttons["Edit"]
+ XCTAssertTrue(editButton.waitForExistence(timeout: 5))
+ editButton.tap()
+
+ // -- Act --
+ let feedbackButton = app.buttons["link-info.feedback.button"]
+ XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5))
+ XCTAssertTrue(feedbackButton.isHittable)
+ feedbackButton.tap()
+
+ // -- Assert --
+ assertFeedbackSubmitButtonExists()
+ }
+
+ func testFeedbackButtonExistsOnCreateListView() throws {
+ // -- Arrange --
+ let createListButton = app.buttons["link-lists.create-list.button"]
+ XCTAssertTrue(createListButton.waitForExistence(timeout: 5))
+ createListButton.tap()
+
+ // -- Act --
+ let feedbackButton = app.buttons["create-link-list.feedback.button"]
+ XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5))
+ XCTAssertTrue(feedbackButton.isHittable)
+ feedbackButton.tap()
+
+ // -- Assert --
+ assertFeedbackSubmitButtonExists()
+ }
+
+ func testFeedbackButtonExistsOnCreateLinkView() throws {
+ // -- Arrange --
+ navigateToListDetail()
+
+ let newLinkButton = app.buttons["link-list-detail.new-link.button"]
+ XCTAssertTrue(newLinkButton.waitForExistence(timeout: 5))
+ newLinkButton.tap()
+
+ // -- Act --
+ let feedbackButton = app.buttons["create-link.feedback.button"]
+ XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5))
+ XCTAssertTrue(feedbackButton.isHittable)
+ feedbackButton.tap()
+
+ // -- Assert --
+ assertFeedbackSubmitButtonExists()
+ }
+
+ // MARK: - Navigation Helpers
+
+ private func navigateToListDetail() {
+ let myLinksRow = app.cells.containing(.staticText, identifier: "My Links").firstMatch
+ XCTAssertTrue(myLinksRow.waitForExistence(timeout: 5))
+ myLinksRow.tap()
+
+ XCTAssertTrue(app.navigationBars["My Links"].waitForExistence(timeout: 5))
+ }
+
+ private func navigateToLinkDetail() {
+ navigateToListDetail()
+
+ let appleRow = app.cells.containing(.staticText, identifier: "Apple").firstMatch
+ XCTAssertTrue(appleRow.waitForExistence(timeout: 5))
+ appleRow.tap()
+ }
+
+ // MARK: - Assertion Helpers
+
+ private func assertFeedbackSubmitButtonExists() {
+ let submitButton = app.buttons["io.sentry.feedback.form.submit"].firstMatch
+ XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
+ }
}