feat: Add debug menu and image diagnostics, improve test coverage
- Add DebugMenuView with network simulation, offline cache management, and Core Data reset - Add OfflineImageDebugView for diagnosing offline image loading issues - Implement debug diagnostics for cached articles and hero image caching - Add cache info display (size, article count) in debug menu - Add shake gesture detection for debug menu access - Fix LocalBookmarksSyncView callback syntax in PhoneTabView - Clean up StringExtensionsTests by removing stripHTMLSimple tests and performance tests - Remove SnapshotHelper import from readeckUITests.swift - Remove snapshot testing code from readeckUITests - Add comprehensive test cases for edge cases (malformed HTML, Unicode, newlines, lists)
This commit is contained in:
parent
8dc5f3000a
commit
fcf6c3e441
318
readeck/UI/Debug/DebugMenuView.swift
Normal file
318
readeck/UI/Debug/DebugMenuView.swift
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
//
|
||||||
|
// DebugMenuView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Claude on 21.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DebugMenuView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@EnvironmentObject private var appSettings: AppSettings
|
||||||
|
@StateObject private var viewModel = DebugMenuViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
// MARK: - Network Section
|
||||||
|
Section {
|
||||||
|
networkSimulationToggle
|
||||||
|
networkStatusInfo
|
||||||
|
} header: {
|
||||||
|
Text("Network Debugging")
|
||||||
|
} footer: {
|
||||||
|
Text("Simulate offline mode to test offline reading features")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Offline Debugging Section
|
||||||
|
Section {
|
||||||
|
Picker("Select Cached Bookmark", selection: $viewModel.selectedBookmarkId) {
|
||||||
|
Text("None").tag(nil as String?)
|
||||||
|
ForEach(viewModel.cachedBookmarks, id: \.id) { bookmark in
|
||||||
|
Text(bookmark.title.isEmpty ? bookmark.id : bookmark.title)
|
||||||
|
.lineLimit(1)
|
||||||
|
.tag(bookmark.id as String?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
OfflineImageDebugView(bookmarkId: viewModel.selectedBookmarkId ?? "")
|
||||||
|
} label: {
|
||||||
|
Label("Offline Image Diagnostics", systemImage: "photo.badge.checkmark")
|
||||||
|
}
|
||||||
|
.disabled(viewModel.selectedBookmarkId == nil)
|
||||||
|
} header: {
|
||||||
|
Text("Offline Reading")
|
||||||
|
} footer: {
|
||||||
|
Text("Select a cached bookmark to diagnose offline image issues")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Logging Section
|
||||||
|
Section {
|
||||||
|
NavigationLink {
|
||||||
|
DebugLogViewer()
|
||||||
|
} label: {
|
||||||
|
Label("View Logs", systemImage: "doc.text.magnifyingglass")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.clearLogs()
|
||||||
|
} label: {
|
||||||
|
Label("Clear All Logs", systemImage: "trash")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Logging")
|
||||||
|
} footer: {
|
||||||
|
Text("View and manage application logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Data Section
|
||||||
|
Section {
|
||||||
|
cacheInfo
|
||||||
|
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.showResetCacheAlert = true
|
||||||
|
} label: {
|
||||||
|
Label("Clear Offline Cache", systemImage: "trash")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.showResetDatabaseAlert = true
|
||||||
|
} label: {
|
||||||
|
Label("Reset Core Data", systemImage: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Data Management")
|
||||||
|
} footer: {
|
||||||
|
Text("⚠️ Reset Core Data will delete all local bookmarks and cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - App Info Section
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Text("App Version")
|
||||||
|
Spacer()
|
||||||
|
Text(viewModel.appVersion)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Build Number")
|
||||||
|
Spacer()
|
||||||
|
Text(viewModel.buildNumber)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Bundle ID")
|
||||||
|
Spacer()
|
||||||
|
Text(viewModel.bundleId)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("App Information")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("🛠️ Debug Menu")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
await viewModel.loadCacheInfo()
|
||||||
|
}
|
||||||
|
.alert("Clear Offline Cache?", isPresented: $viewModel.showResetCacheAlert) {
|
||||||
|
Button("Cancel", role: .cancel) { }
|
||||||
|
Button("Clear", role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await viewModel.clearOfflineCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("This will remove all cached articles. Your bookmarks will remain.")
|
||||||
|
}
|
||||||
|
.alert("Reset Core Data?", isPresented: $viewModel.showResetDatabaseAlert) {
|
||||||
|
Button("Cancel", role: .cancel) { }
|
||||||
|
Button("Reset", role: .destructive) {
|
||||||
|
viewModel.resetCoreData()
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("⚠️ WARNING: This will delete ALL local data including bookmarks, cache, and settings. This cannot be undone!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subviews
|
||||||
|
|
||||||
|
private var networkSimulationToggle: some View {
|
||||||
|
Toggle(isOn: Binding(
|
||||||
|
get: { !appSettings.isNetworkConnected },
|
||||||
|
set: { isOffline in
|
||||||
|
appSettings.isNetworkConnected = !isOffline
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: appSettings.isNetworkConnected ? "wifi" : "wifi.slash")
|
||||||
|
.foregroundColor(appSettings.isNetworkConnected ? .green : .orange)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Simulate Offline Mode")
|
||||||
|
Text(appSettings.isNetworkConnected ? "Network Connected" : "Network Disconnected")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var networkStatusInfo: some View {
|
||||||
|
HStack {
|
||||||
|
Text("Network Status")
|
||||||
|
Spacer()
|
||||||
|
Label(
|
||||||
|
appSettings.isNetworkConnected ? "Connected" : "Offline",
|
||||||
|
systemImage: appSettings.isNetworkConnected ? "checkmark.circle.fill" : "xmark.circle.fill"
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(appSettings.isNetworkConnected ? .green : .orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cacheInfo: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Cached Articles")
|
||||||
|
Spacer()
|
||||||
|
Text("\(viewModel.cachedArticlesCount)")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Cache Size")
|
||||||
|
Spacer()
|
||||||
|
Text(viewModel.cacheSize)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
await viewModel.loadCacheInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class DebugMenuViewModel: ObservableObject {
|
||||||
|
@Published var showResetCacheAlert = false
|
||||||
|
@Published var showResetDatabaseAlert = false
|
||||||
|
@Published var cachedArticlesCount = 0
|
||||||
|
@Published var cacheSize = "0 KB"
|
||||||
|
@Published var selectedBookmarkId: String?
|
||||||
|
@Published var cachedBookmarks: [Bookmark] = []
|
||||||
|
|
||||||
|
private let offlineCacheRepository = OfflineCacheRepository()
|
||||||
|
private let coreDataManager = CoreDataManager.shared
|
||||||
|
private let logger = Logger.general
|
||||||
|
|
||||||
|
var appVersion: String {
|
||||||
|
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildNumber: String {
|
||||||
|
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
var bundleId: String {
|
||||||
|
Bundle.main.bundleIdentifier ?? "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCacheInfo() async {
|
||||||
|
cachedArticlesCount = offlineCacheRepository.getCachedArticlesCount()
|
||||||
|
cacheSize = offlineCacheRepository.getCacheSize()
|
||||||
|
|
||||||
|
// Load cached bookmarks for diagnostics
|
||||||
|
do {
|
||||||
|
cachedBookmarks = try await offlineCacheRepository.getCachedBookmarks()
|
||||||
|
// Auto-select first bookmark if available
|
||||||
|
if selectedBookmarkId == nil, let firstBookmark = cachedBookmarks.first {
|
||||||
|
selectedBookmarkId = firstBookmark.id
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to load cached bookmarks: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearOfflineCache() async {
|
||||||
|
do {
|
||||||
|
try await offlineCacheRepository.clearCache()
|
||||||
|
await loadCacheInfo()
|
||||||
|
logger.info("Offline cache cleared via Debug Menu")
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to clear offline cache: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearLogs() {
|
||||||
|
// TODO: Implement log clearing when we add persistent logging
|
||||||
|
logger.info("Logs cleared via Debug Menu")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetCoreData() {
|
||||||
|
do {
|
||||||
|
try coreDataManager.resetDatabase()
|
||||||
|
logger.warning("Core Data reset via Debug Menu - App restart required")
|
||||||
|
|
||||||
|
// Show alert that restart is needed
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
fatalError("Core Data has been reset. Please restart the app.")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to reset Core Data: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Shake Gesture Detection
|
||||||
|
|
||||||
|
extension UIDevice {
|
||||||
|
static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification")
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIWindow {
|
||||||
|
open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
|
||||||
|
if motion == .motionShake {
|
||||||
|
NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeviceShakeViewModifier: ViewModifier {
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.onAppear()
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func onShake(perform action: @escaping () -> Void) -> some View {
|
||||||
|
self.modifier(DeviceShakeViewModifier(action: action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
DebugMenuView()
|
||||||
|
.environmentObject(AppSettings())
|
||||||
|
}
|
||||||
|
#endif
|
||||||
199
readeck/UI/Debug/OfflineImageDebugView.swift
Normal file
199
readeck/UI/Debug/OfflineImageDebugView.swift
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
//
|
||||||
|
// OfflineImageDebugView.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Debug view to diagnose offline image loading issues
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct OfflineImageDebugView: View {
|
||||||
|
let bookmarkId: String
|
||||||
|
|
||||||
|
@State private var debugInfo: DebugInfo = DebugInfo()
|
||||||
|
@EnvironmentObject var appSettings: AppSettings
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("Offline Image Debug")
|
||||||
|
.font(.title)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Group {
|
||||||
|
DebugSection("Network Status") {
|
||||||
|
InfoRow(label: "Connected", value: "\(appSettings.isNetworkConnected)")
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugSection("Cached Article") {
|
||||||
|
InfoRow(label: "Has Cache", value: "\(debugInfo.hasCachedHTML)")
|
||||||
|
InfoRow(label: "HTML Size", value: debugInfo.htmlSize)
|
||||||
|
InfoRow(label: "Base64 Images", value: "\(debugInfo.base64ImageCount)")
|
||||||
|
InfoRow(label: "HTTP Images", value: "\(debugInfo.httpImageCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugSection("Hero Image Cache") {
|
||||||
|
InfoRow(label: "URL", value: debugInfo.heroImageURL)
|
||||||
|
InfoRow(label: "In Cache", value: "\(debugInfo.heroImageInCache)")
|
||||||
|
InfoRow(label: "Cache Key", value: debugInfo.cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !debugInfo.sampleImages.isEmpty {
|
||||||
|
DebugSection("Sample HTML Images") {
|
||||||
|
ForEach(debugInfo.sampleImages.indices, id: \.self) { index in
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Image \(index + 1)")
|
||||||
|
.font(.caption).bold()
|
||||||
|
Text(debugInfo.sampleImages[index])
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Button("Run Diagnostics") {
|
||||||
|
Task {
|
||||||
|
await runDiagnostics()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
await runDiagnostics()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runDiagnostics() async {
|
||||||
|
let offlineCache = OfflineCacheRepository()
|
||||||
|
|
||||||
|
// Check cached HTML
|
||||||
|
if let cachedHTML = offlineCache.getCachedArticle(id: bookmarkId) {
|
||||||
|
debugInfo.hasCachedHTML = true
|
||||||
|
debugInfo.htmlSize = ByteCountFormatter.string(fromByteCount: Int64(cachedHTML.utf8.count), countStyle: .file)
|
||||||
|
|
||||||
|
// Count Base64 images
|
||||||
|
debugInfo.base64ImageCount = countMatches(in: cachedHTML, pattern: #"src="data:image/"#)
|
||||||
|
|
||||||
|
// Count HTTP images
|
||||||
|
debugInfo.httpImageCount = countMatches(in: cachedHTML, pattern: #"src="https?://"#)
|
||||||
|
|
||||||
|
// Extract sample image URLs
|
||||||
|
debugInfo.sampleImages = extractSampleImages(from: cachedHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check hero image cache
|
||||||
|
do {
|
||||||
|
let bookmarkDetail = try await DefaultUseCaseFactory.shared.makeGetBookmarkUseCase().execute(id: bookmarkId)
|
||||||
|
|
||||||
|
if !bookmarkDetail.imageUrl.isEmpty, let url = URL(string: bookmarkDetail.imageUrl) {
|
||||||
|
debugInfo.heroImageURL = bookmarkDetail.imageUrl
|
||||||
|
debugInfo.cacheKey = url.cacheKey
|
||||||
|
|
||||||
|
// Check if image is in Kingfisher cache
|
||||||
|
let isCached = await withCheckedContinuation { continuation in
|
||||||
|
KingfisherManager.shared.cache.retrieveImage(forKey: url.cacheKey) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let cacheResult):
|
||||||
|
continuation.resume(returning: cacheResult.image != nil)
|
||||||
|
case .failure:
|
||||||
|
continuation.resume(returning: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugInfo.heroImageInCache = isCached
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error loading bookmark: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func countMatches(in text: String, pattern: String) -> Int {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return 0 }
|
||||||
|
let nsString = text as NSString
|
||||||
|
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
|
||||||
|
return matches.count
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractSampleImages(from html: String) -> [String] {
|
||||||
|
let pattern = #"<img[^>]+src="([^"]+)""#
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return [] }
|
||||||
|
|
||||||
|
let nsString = html as NSString
|
||||||
|
let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length))
|
||||||
|
|
||||||
|
return matches.prefix(3).compactMap { match in
|
||||||
|
guard match.numberOfRanges >= 2 else { return nil }
|
||||||
|
let urlRange = match.range(at: 1)
|
||||||
|
let url = nsString.substring(with: urlRange)
|
||||||
|
|
||||||
|
// Truncate long Base64 strings
|
||||||
|
if url.hasPrefix("data:image/") {
|
||||||
|
return "data:image/... (Base64, \(url.count) chars)"
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DebugInfo {
|
||||||
|
var hasCachedHTML = false
|
||||||
|
var htmlSize = "0 KB"
|
||||||
|
var base64ImageCount = 0
|
||||||
|
var httpImageCount = 0
|
||||||
|
var heroImageURL = "N/A"
|
||||||
|
var heroImageInCache = false
|
||||||
|
var cacheKey = "N/A"
|
||||||
|
var sampleImages: [String] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InfoRow: View {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text(value)
|
||||||
|
.font(.subheadline.bold())
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DebugSection<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(_ title: String, @ViewBuilder content: () -> Content) {
|
||||||
|
self.title = title
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(title)
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
content
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
OfflineImageDebugView(bookmarkId: "123")
|
||||||
|
.environmentObject(AppSettings())
|
||||||
|
}
|
||||||
@ -206,9 +206,9 @@ struct PhoneTabView: View {
|
|||||||
} else {
|
} else {
|
||||||
Section {
|
Section {
|
||||||
VStack {
|
VStack {
|
||||||
LocalBookmarksSyncView(state: offlineBookmarksViewModel.state) {
|
LocalBookmarksSyncView(state: offlineBookmarksViewModel.state, onSyncTapped: {
|
||||||
await offlineBookmarksViewModel.syncOfflineBookmarks()
|
await offlineBookmarksViewModel.syncOfflineBookmarks()
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
.listRowInsets(EdgeInsets())
|
.listRowInsets(EdgeInsets())
|
||||||
|
|||||||
@ -59,73 +59,6 @@ final class StringExtensionsTests: XCTestCase {
|
|||||||
XCTAssertEqual(onlyTags.stripHTML, expected)
|
XCTAssertEqual(onlyTags.stripHTML, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - stripHTMLSimple Tests
|
|
||||||
|
|
||||||
func testStripHTMLSimple_BasicTags() {
|
|
||||||
let html = "<p>Text mit <strong>fett</strong>.</p>"
|
|
||||||
let expected = "Text mit fett."
|
|
||||||
|
|
||||||
XCTAssertEqual(html.stripHTMLSimple, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testStripHTMLSimple_HTMLEntities() {
|
|
||||||
let html = "<p>Text mit Leerzeichen, & Zeichen und "Anführungszeichen".</p>"
|
|
||||||
let expected = "Text mit Leerzeichen, & Zeichen und \"Anführungszeichen\"."
|
|
||||||
|
|
||||||
XCTAssertEqual(html.stripHTMLSimple, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testStripHTMLSimple_MoreEntities() {
|
|
||||||
let html = "<p><Tag> und 'Apostroph'</p>"
|
|
||||||
let expected = "<Tag> und 'Apostroph'"
|
|
||||||
|
|
||||||
XCTAssertEqual(html.stripHTMLSimple, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testStripHTMLSimple_ComplexHTML() {
|
|
||||||
let html = "<div class=\"container\"><h1>Überschrift</h1><p>Absatz mit <em>kursiv</em> und <strong>fett</strong>.</p><ul><li>Liste 1</li><li>Liste 2</li></ul></div>"
|
|
||||||
let expected = "Überschrift\nAbsatz mit kursiv und fett.\nListe 1\nListe 2"
|
|
||||||
|
|
||||||
XCTAssertEqual(html.stripHTMLSimple, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testStripHTMLSimple_NoTags() {
|
|
||||||
let plainText = "Normaler Text ohne HTML."
|
|
||||||
|
|
||||||
XCTAssertEqual(plainText.stripHTMLSimple, plainText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testStripHTMLSimple_EmptyString() {
|
|
||||||
let emptyString = ""
|
|
||||||
|
|
||||||
XCTAssertEqual(emptyString.stripHTMLSimple, emptyString)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testStripHTMLSimple_WhitespaceHandling() {
|
|
||||||
let html = " <p> Text mit Whitespace </p> "
|
|
||||||
let expected = "Text mit Whitespace"
|
|
||||||
|
|
||||||
XCTAssertEqual(html.stripHTMLSimple, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Performance Tests
|
|
||||||
|
|
||||||
func testStripHTML_Performance() {
|
|
||||||
let largeHTML = String(repeating: "<p>Dies ist ein Test mit <strong>vielen</strong> <em>HTML</em> Tags.</p>", count: 1000)
|
|
||||||
|
|
||||||
measure {
|
|
||||||
_ = largeHTML.stripHTML
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testStripHTMLSimple_Performance() {
|
|
||||||
let largeHTML = String(repeating: "<p>Dies ist ein Test mit <strong>vielen</strong> <em>HTML</em> Tags.</p>", count: 1000)
|
|
||||||
|
|
||||||
measure {
|
|
||||||
_ = largeHTML.stripHTMLSimple
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Edge Cases
|
// MARK: - Edge Cases
|
||||||
|
|
||||||
func testStripHTML_MalformedHTML() {
|
func testStripHTML_MalformedHTML() {
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
import SnapshotHelper
|
|
||||||
|
|
||||||
final class readeckUITests: XCTestCase {
|
final class readeckUITests: XCTestCase {
|
||||||
|
|
||||||
@ -16,7 +15,7 @@ final class readeckUITests: XCTestCase {
|
|||||||
// 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.
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
override func tearDownWithError() throws {
|
||||||
@ -27,9 +26,7 @@ final class readeckUITests: XCTestCase {
|
|||||||
func testExample() throws {
|
func testExample() throws {
|
||||||
// UI tests must launch the application that they test.
|
// UI tests must launch the application that they test.
|
||||||
let app = XCUIApplication()
|
let app = XCUIApplication()
|
||||||
setupSnapshot(app)
|
|
||||||
app.launch()
|
app.launch()
|
||||||
snapshot("01LaunchScreen")
|
|
||||||
|
|
||||||
// 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.
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user