From a70b754c93c8709dc908abf3b539d302b862d136 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 23 Feb 2024 17:35:47 +0600 Subject: [PATCH 01/23] PDF saving, printing --- DuckDuckGo.xcodeproj/project.pbxproj | 8 + .../Extensions/FileManagerExtension.swift | 6 + .../Extensions/WKPDFHUDViewWrapper.swift | 91 +++++++++++ .../Extensions/WKWebViewExtension.swift | 10 ++ .../Model/FileDownloadManager.swift | 7 +- DuckDuckGo/InfoPlist.xcstrings | 4 +- DuckDuckGo/MainWindow/MainView.swift | 26 +++ DuckDuckGo/Menus/MainMenuActions.swift | 25 ++- DuckDuckGo/Tab/Model/Tab+UIDelegate.swift | 42 ++++- .../TabExtensions/DownloadsTabExtension.swift | 153 +++++++++++++----- .../DownloadsTabExtensionTests.swift | 70 ++++++-- .../Helpers/DownloadsTabExtensionMock.swift | 8 +- ...bViewPrivateMethodsAvailabilityTests.swift | 6 + 13 files changed, 393 insertions(+), 63 deletions(-) create mode 100644 DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0a586a3376..ece8bf2786 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2741,6 +2741,9 @@ B658BAB62B0F845D00D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; }; B658BAB72B0F848D00D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; }; B658BAB92B0F849100D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; }; + B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; }; + B65C7DFC2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; }; + B65C7DFD2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; }; B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CA2B316DF100A595BB /* SnapshotTesting */; }; B65CD8CD2B316DFC00A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CC2B316DFC00A595BB /* SnapshotTesting */; }; B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CE2B316E0200A595BB /* SnapshotTesting */; }; @@ -4234,6 +4237,7 @@ B657841925FA484B00D8DB33 /* NSException+Catch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSException+Catch.m"; sourceTree = ""; }; B657841E25FA497600D8DB33 /* NSException+Catch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSException+Catch.swift"; sourceTree = ""; }; B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKPDFHUDViewWrapper.swift; sourceTree = ""; }; B65CD8D42B316FCA00A595BB /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; B65CD8D72B341FD300A595BB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; B65E6B9D26D9EC0800095F96 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; @@ -7634,6 +7638,7 @@ B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */, B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */, B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, + B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */, B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, B63D466725BEB6C200874977 /* WKWebView+Private.h */, @@ -9709,6 +9714,7 @@ 1D36E659298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, B6BCC5242AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */, 3706FEBC293F6EFF00E42796 /* BWResponse.swift in Sources */, + B65C7DFC2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, 3706FAF4293F65D500E42796 /* SafariBookmarksReader.swift in Sources */, 31C9ADE62AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */, B65211262B29A42E00B30633 /* BookmarkStoreMock.swift in Sources */, @@ -10964,6 +10970,7 @@ 4B957A192AC7AE700062CA31 /* PasswordManagerCoordinator.swift in Sources */, 4B957A1A2AC7AE700062CA31 /* PasswordManagementIdentityModel.swift in Sources */, 4B957A1B2AC7AE700062CA31 /* UserDefaultsWrapper.swift in Sources */, + B65C7DFD2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, 4B957A1C2AC7AE700062CA31 /* PasswordManagementPopover.swift in Sources */, 4B957A1D2AC7AE700062CA31 /* BWCommunicator.swift in Sources */, 4B957A1E2AC7AE700062CA31 /* HomePageRecentlyVisitedModel.swift in Sources */, @@ -11582,6 +11589,7 @@ AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */, 1D43EB3A292B63B00065E5D6 /* BWRequest.swift in Sources */, B684121C2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, + B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, B68458CD25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift in Sources */, 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */, B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */, diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift index 5b39680e13..4705706483 100644 --- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift +++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift @@ -21,10 +21,16 @@ import Foundation extension FileManager { + @discardableResult func moveItem(at srcURL: URL, to destURL: URL, incrementingIndexIfExists flag: Bool, pathExtension: String? = nil) throws -> URL { return try self.perform(self.moveItem, from: srcURL, to: destURL, incrementingIndexIfExists: flag, pathExtension: pathExtension) } + @discardableResult + func copyItem(at srcURL: URL, to destURL: URL, incrementingIndexIfExists flag: Bool, pathExtension: String? = nil) throws -> URL { + return try self.perform(self.copyItem, from: srcURL, to: destURL, incrementingIndexIfExists: flag, pathExtension: pathExtension) + } + private func perform(_ operation: (URL, URL) throws -> Void, from srcURL: URL, to destURL: URL, diff --git a/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift new file mode 100644 index 0000000000..e93ea14221 --- /dev/null +++ b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift @@ -0,0 +1,91 @@ +// +// WKPDFHUDViewWrapper.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit + +/// A wrapper for the PDF HUD window with Zoom controls, Download and Open in Preview buttons +/// Used to trigger Save PDF +struct WKPDFHUDViewWrapper { + + static let WKPDFHUDViewClass: AnyClass? = NSClassFromString("WKPDFHUDView") + static let performActionForControlSelector = NSSelectorFromString("_performActionForControl:") + static let visibleKey = "_visible" + static let setVisibleSelector = NSSelectorFromString("_setVisible:") + static let savePDFControlId = "arrow.down.circle" + + private let hudView: NSView + + var isVisible: Bool { + get { + hudView.layer?.sublayers?.first?.opacity ?? 0 > 0 + } + nonmutating set { + guard hudView.responds(to: Self.setVisibleSelector) else { return } + hudView.perform(Self.setVisibleSelector, with: newValue) + } + } + + init?(view: NSView) { + guard type(of: view) == Self.WKPDFHUDViewClass else { return nil } + + guard Self.WKPDFHUDViewClass?.instancesRespond(to: Self.performActionForControlSelector) == true else { + assertionFailure("WKPDFHUDView doesn‘t respond to _performActionForControl:") + return nil + } + self.hudView = view + } + + init?(webView: WKWebView, location: NSPoint? = nil) { + guard let hudView = webView.subviews.last(where: { type(of: $0) == Self.WKPDFHUDViewClass && $0.frame.contains(location ?? $0.frame.origin) }) else { +#if DEBUG + Task { + if await webView.mimeType == "application/pdf" { + assertionFailure("WebView doesn‘t have PDF HUD View") + } + } +#endif + return nil + } + self.init(view: hudView) + } + + func savePDF() { + let wasVisible = isVisible + self.setIsVisibleIVar(true) + defer { + if !wasVisible { + self.setIsVisibleIVar(false) + } + } + hudView.perform(Self.performActionForControlSelector, with: Self.savePDFControlId) + } + + // try to set _visible ivar value directly to avoid actually showing the HUD + private func setIsVisibleIVar(_ value: Bool) { + do { + try NSException.catch { + hudView.setValue(value, forKey: Self.visibleKey) + } + } catch { + assertionFailure("\(error)") + self.isVisible = value + } + } + +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 4c70d4bacc..f581f31243 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -325,6 +325,16 @@ extension WKWebView { return self.printOperation(with: printInfo) } + func hudView(at point: NSPoint? = nil) -> WKPDFHUDViewWrapper? { + WKPDFHUDViewWrapper(webView: self, location: point) + } + + func savePDF(_ pdfHUD: WKPDFHUDViewWrapper? = nil) -> Bool { + guard let hudView = pdfHUD ?? hudView() else { return false } + hudView.savePDF() + return true + } + var fullScreenPlaceholderView: NSView? { guard self.responds(to: Selector.fullScreenPlaceholderView) else { return nil } return self.value(forKey: NSStringFromSelector(Selector.fullScreenPlaceholderView)) as? NSView diff --git a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift index 9ae1945b25..7e8aacc576 100644 --- a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift +++ b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift @@ -83,10 +83,11 @@ final class FileDownloadManager: FileDownloadManagerProtocol { return url } - var promptForLocation: Bool { + func shouldPromptForLocation(for url: URL?) -> Bool { switch self { case .prompt: return true - case .preset, .auto: return false + case .preset: return false + case .auto: return (url?.isFileURL ?? true || url?.isLocalURL ?? true) // always prompt when "downloading" a local file } } } @@ -96,7 +97,7 @@ final class FileDownloadManager: FileDownloadManagerProtocol { dispatchPrecondition(condition: .onQueue(.main)) let task = WebKitDownloadTask(download: download, - promptForLocation: location.promptForLocation, + promptForLocation: location.shouldPromptForLocation(for: download.originalRequest?.url), destinationURL: location.destinationURL, tempURL: location.tempURL, isBurner: fromBurnerWindow) diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index 70d7389fb7..bfcde1d533 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -8,7 +8,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "DuckDuckGo Privacy Pro" + "value" : "DuckDuckGo" } } } @@ -75,4 +75,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/MainWindow/MainView.swift b/DuckDuckGo/MainWindow/MainView.swift index 23a92c2e0a..a61376a296 100644 --- a/DuckDuckGo/MainWindow/MainView.swift +++ b/DuckDuckGo/MainWindow/MainView.swift @@ -109,6 +109,7 @@ final class MainView: NSView { // PDF Plugin context menu override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { setupSearchContextMenuItem(menu: menu) + setupSaveAsAndPrintMenuItems(menu: menu, with: event) } private func setupSearchContextMenuItem(menu: NSMenu) { @@ -132,6 +133,31 @@ final class MainView: NSView { } } + private func setupSaveAsAndPrintMenuItems(menu: NSMenu, with event: NSEvent) { + let hudView: WKPDFHUDViewWrapper? = withMouseLocationInViewCoordinates(event.locationInWindow) { point in + guard let view = self.hitTest(point) else { return nil } + if let hudView = WKPDFHUDViewWrapper(view: view) { + return hudView + } else if let webView = view as? WKWebView { + return webView.hudView(at: webView.convert(point, from: self)) + } + return nil + } + + // Insert Save As… and Print… items + let copyItemIdx = menu.indexOfItem(withTitle: UserText.copy) + let separatorAfterCopyItemIdx = ( + menu.items.indices.contains(copyItemIdx) + ? (copyItemIdx.. NSDragOperation { diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 691fef0c0f..eb9ff78c8c 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -621,13 +621,34 @@ extension MainViewController { // MARK: - Printing @objc func printWebView(_ sender: Any?) { - getActiveTabAndIndex()?.tab.print() + var pdfHUD: WKPDFHUDViewWrapper? + // if saving a PDF (may be from a frame) + if let menuItem = sender as? NSMenuItem, + let representedObject = menuItem.representedObject { + + pdfHUD = representedObject as? WKPDFHUDViewWrapper ?? { + assertionFailure("Unexpected Save As menu item represented object: \(representedObject)") + return nil + }() + } + + getActiveTabAndIndex()?.tab.print(pdfHUD: pdfHUD) } // MARK: - Saving @objc func saveAs(_ sender: Any) { - getActiveTabAndIndex()?.tab.saveWebContentAs() + var pdfHUD: WKPDFHUDViewWrapper? + // if saving a PDF (may be from a frame) + if let menuItem = sender as? NSMenuItem, + let representedObject = menuItem.representedObject { + + pdfHUD = representedObject as? WKPDFHUDViewWrapper ?? { + assertionFailure("Unexpected Save As menu item represented object: \(representedObject)") + return nil + }() + } + getActiveTabAndIndex()?.tab.saveWebContent(pdfHUD: pdfHUD, location: .prompt) } // MARK: - Debug diff --git a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift index 97133045a0..482d8b71f1 100644 --- a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift +++ b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift @@ -22,6 +22,7 @@ import Foundation import Navigation import UniformTypeIdentifiers import WebKit +import PDFKit extension Tab: WKUIDelegate, PrintingUserScriptDelegate { @@ -35,9 +36,22 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { self.value(forKey: Tab.objcNewWindowPolicyDecisionMakersKeyPath) as? [NewWindowPolicyDecisionMaker] } + @MainActor private static var expectedSaveDataToFileCallback: (@MainActor (URL?) -> Void)? + @MainActor + private static func consumeExpectedSaveDataToFileCallback() -> (@MainActor (URL?) -> Void)? { + defer { + expectedSaveDataToFileCallback = nil + } + return expectedSaveDataToFileCallback + } + @objc(_webView:saveDataToFile:suggestedFilename:mimeType:originatingURL:) func webView(_ webView: WKWebView, saveDataToFile data: Data, suggestedFilename: String, mimeType: String, originatingURL: URL) { - saveDownloaded(data: data, suggestedFilename: suggestedFilename, mimeType: mimeType) + Task { + let result = try? await saveDownloadedData(data, suggestedFilename: suggestedFilename, mimeType: mimeType, originatingURL: originatingURL) + // when print function saves a PDF setting the callback, return the saved temporary file to it + await Self.consumeExpectedSaveDataToFileCallback()?(result) + } } @MainActor @@ -299,6 +313,10 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { printOperation.view?.frame = webView.bounds } + runPrintOperation(printOperation, completionHandler: completionHandler) + } + + func runPrintOperation(_ printOperation: NSPrintOperation, completionHandler: ((Bool) -> Void)? = nil) { let dialog = UserDialogType.print(.init(printOperation) { result in completionHandler?((try? result.get()) ?? false) }) @@ -315,7 +333,27 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { self.runPrintOperation(for: frameHandle, in: webView) { _ in completionHandler() } } - func print() { + @MainActor(unsafe) + func print(pdfHUD: WKPDFHUDViewWrapper? = nil) { + if let pdfHUD { + Self.expectedSaveDataToFileCallback = { [weak self] url in + guard let self, let url, + let pdfDocument = PDFDocument(url: url) else { + assertionFailure("Could not load PDF document from \(url?.path ?? "")") + return + } + // Set up NSPrintOperation + guard let printOperation = pdfDocument.printOperation(for: .shared, scalingMode: .pageScaleNone, autoRotate: false) else { + assertionFailure("Could not print PDF document") + return + } + + self.runPrintOperation(printOperation) + } + saveWebContent(pdfHUD: pdfHUD, location: .temporary) + return + } + self.runPrintOperation(for: nil, in: self.webView) } diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index ab54bb1701..248665c18b 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -33,10 +33,17 @@ final class DownloadsTabExtension: NSObject { private let isBurner: Bool private let downloadsPreferences: DownloadsPreferences + enum DownloadLocation { + case auto + case prompt + case temporary + } + private var nextSaveDataRequestDownloadLocation: DownloadLocation = .auto + @Published private(set) var savePanelDialogRequest: SavePanelDialogRequest? { - didSet { - savePanelDialogRequest?.addCompletionHandler { [weak self, weak savePanelDialogRequest] _ in + willSet { + newValue?.addCompletionHandler { [weak self, weak savePanelDialogRequest] _ in if let self, let savePanelDialogRequest, self.savePanelDialogRequest === savePanelDialogRequest { @@ -56,44 +63,88 @@ final class DownloadsTabExtension: NSObject { super.init() } - func saveWebViewContentAs(_ webView: WKWebView) { + func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadLocation) { Task { @MainActor in - await saveWebViewContentAs(webView) + await saveWebViewContent(from: webView, pdfHUD: pdfHUD, location: location) } } @MainActor - private func saveWebViewContentAs(_ webView: WKWebView) async { - guard await webView.mimeType == UTType.html.preferredMIMEType else { - if let url = webView.url { - webView.startDownload(using: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)) { download in - self.downloadManager.add(download, - fromBurnerWindow: self.isBurner, - delegate: self, location: .prompt) - } + private func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadLocation) async { + let mimeType = pdfHUD != nil ? UTType.pdf.preferredMIMEType : await webView.mimeType + switch mimeType { + case UTType.html.preferredMIMEType: + assert([.prompt, .auto].contains(location)) + + let parameters = SavePanelParameters(suggestedFilename: webView.suggestedFilename, fileTypes: [.html, .webArchive, .pdf]) + self.savePanelDialogRequest = SavePanelDialogRequest(parameters) { result in + guard let (url, fileType) = try? result.get() else { return } + webView.exportWebContent(to: url, as: fileType.flatMap(WKWebView.ContentExportType.init) ?? .html) } - return + + case UTType.pdf.preferredMIMEType: + self.nextSaveDataRequestDownloadLocation = location + let success = webView.savePDF(pdfHUD) // calls `saveDownloadedData(_:suggestedFilename:mimeType:originatingURL)` + guard success else { fallthrough } + + default: + guard let url = webView.url else { + assertionFailure("Can‘t save web content without URL loaded") + return + } + if url.isFileURL { + self.nextSaveDataRequestDownloadLocation = location + _=try? await self.saveDownloadedData(nil, suggestedFilename: url.lastPathComponent, mimeType: mimeType ?? "text/html", originatingURL: url) + return + } + + let download = await webView.startDownload(using: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)) + + let location = self.downloadLocation(for: location, suggestedFilename: download.webView?.suggestedFilename ?? "") + self.downloadManager.add(download, + fromBurnerWindow: self.isBurner, + delegate: self, + location: location) } - let parameters = SavePanelParameters(suggestedFilename: webView.suggestedFilename, fileTypes: [.html, .webArchive, .pdf]) - self.savePanelDialogRequest = SavePanelDialogRequest(parameters) { result in - guard let (url, fileType) = try? result.get() else { return } - webView.exportWebContent(to: url, as: fileType.flatMap(WKWebView.ContentExportType.init) ?? .html) + } + + private func downloadLocation(for location: DownloadLocation, suggestedFilename: String) -> FileDownloadManager.DownloadLocationPreference { + switch location { + case .auto: + return .auto + case .prompt: + return .prompt + case .temporary: + let suggestedFilename = suggestedFilename.isEmpty ? UUID().uuidString : suggestedFilename + let fm = FileManager.default + let dirURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename()) + try? fm.createDirectory(at: dirURL, withIntermediateDirectories: true) + return .preset(destinationURL: dirURL.appendingPathComponent(suggestedFilename), tempURL: nil) } } - private func saveDownloaded(data: Data, to toURL: URL) { + private func saveDownloadedData(_ data: Data?, to toURL: URL, originatingURL: URL) throws { let fm = FileManager.default - let tempURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename()) - do { - // First save file in a temporary directory - try data.write(to: tempURL) - // Then move the file to the download location and show a bounce if the file is in a location on the user's dock. + + // if no data provided - copy file from local url to the destination url + guard let data else { + guard originatingURL.isFileURL else { + assertionFailure("No data provided for non-file URL") + return + } try Progress.withPublishedProgress(url: toURL) { - _ = try fm.moveItem(at: tempURL, to: toURL, incrementingIndexIfExists: true) + try fm.copyItem(at: originatingURL, to: toURL, incrementingIndexIfExists: true) } - } catch { - os_log("Failed to save PDF file to Downloads folder", type: .error) + return + } + + let tempURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename()) + // First save file in a temporary directory + try data.write(to: tempURL) + // Then move the file to the download location and show a bounce if the file is in a location on the user's dock. + try Progress.withPublishedProgress(url: toURL) { + try fm.moveItem(at: tempURL, to: toURL, incrementingIndexIfExists: true) } } @@ -115,7 +166,7 @@ extension DownloadsTabExtension: NavigationResponder { @MainActor func decidePolicy(for navigationResponse: NavigationResponse) async -> NavigationResponsePolicy? { - guard navigationResponse.httpResponse?.isSuccessful == true, + guard navigationResponse.httpResponse?.isSuccessful != false, !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload else { return .next @@ -210,9 +261,9 @@ protocol DownloadsTabExtensionProtocol: AnyObject, NavigationResponder, Download var delegate: TabDownloadsDelegate? { get set } var savePanelDialogPublisher: AnyPublisher { get } - func saveWebViewContentAs(_ webView: WKWebView) + func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) + func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? } extension DownloadsTabExtension: TabExtension, DownloadsTabExtensionProtocol { @@ -225,19 +276,35 @@ extension DownloadsTabExtension: TabExtension, DownloadsTabExtensionProtocol { } @MainActor - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) { - if !downloadsPreferences.alwaysRequestDownloadLocation, - let location = downloadsPreferences.effectiveDownloadLocation { - let url = location.appendingPathComponent(suggestedFilename) - saveDownloaded(data: data, to: url) - return + func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? { + defer { + self.nextSaveDataRequestDownloadLocation = .auto } + switch downloadLocation(for: nextSaveDataRequestDownloadLocation, suggestedFilename: suggestedFilename) { + case .auto: + guard !downloadsPreferences.alwaysRequestDownloadLocation, + let location = downloadsPreferences.effectiveDownloadLocation else { fallthrough /* prompt */ } + + let url = location.appendingPathComponent(suggestedFilename) + try saveDownloadedData(data, to: url, originatingURL: originatingURL) + return url + + case .prompt: + let fileTypes = UTType(mimeType: mimeType).map { [$0] } ?? [] + let url: URL? = await withCheckedContinuation { continuation in + chooseDestination(suggestedFilename: suggestedFilename, directoryURL: nil, fileTypes: fileTypes) { url, _ in + continuation.resume(returning: url) + } + } + + guard let url else { return nil } - let fileTypes = UTType(mimeType: mimeType).map { [$0] } ?? [] - chooseDestination(suggestedFilename: suggestedFilename, directoryURL: nil, fileTypes: fileTypes) { [weak self] url, _ in - guard let url else { return } + try saveDownloadedData(data, to: url, originatingURL: originatingURL) + return url - self?.saveDownloaded(data: data, to: url) + case .preset(destinationURL: let destinationURL, tempURL: _): + try saveDownloadedData(data, to: destinationURL, originatingURL: originatingURL) + return destinationURL } } } @@ -250,12 +317,12 @@ extension TabExtensions { extension Tab { - func saveWebContentAs() { - self.downloads?.saveWebViewContentAs(webView) + func saveWebContent(pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) { + self.downloads?.saveWebViewContent(from: webView, pdfHUD: pdfHUD, location: location) } - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) { - self.downloads?.saveDownloaded(data: data, suggestedFilename: suggestedFilename, mimeType: mimeType) + func saveDownloadedData(_ data: Data, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? { + try await self.downloads?.saveDownloadedData(data, suggestedFilename: suggestedFilename, mimeType: mimeType, originatingURL: originatingURL) } } diff --git a/UnitTests/FileDownload/DownloadsTabExtensionTests.swift b/UnitTests/FileDownload/DownloadsTabExtensionTests.swift index 55b835e12f..c3747aea31 100644 --- a/UnitTests/FileDownload/DownloadsTabExtensionTests.swift +++ b/UnitTests/FileDownload/DownloadsTabExtensionTests.swift @@ -22,8 +22,10 @@ import UniformTypeIdentifiers @testable import DuckDuckGo_Privacy_Browser +@MainActor final class DownloadsTabExtensionTests: XCTestCase { private var testData: Data! + private var testOriginatingURL: URL! private let filename = "Document.pdf" private let fileManager = FileManager.default private var testDirectory: URL! @@ -34,6 +36,9 @@ final class DownloadsTabExtensionTests: XCTestCase { testData = try XCTUnwrap("test".data(using: .utf8)) testDirectory = fileManager.temporaryDirectory + testOriginatingURL = testDirectory.appendingPathComponent(UUID().uuidString + ".pdf") + try testData.write(to: testOriginatingURL) + cancellables = [] } @@ -44,27 +49,61 @@ final class DownloadsTabExtensionTests: XCTestCase { try super.tearDownWithError() } - @MainActor - func testWhenAlwaysRequestDownloadLocationIsTrueThenShouldAskDownloadsTabExtensionToSaveData() throws { + func testWhenAlwaysRequestDownloadLocationIsTrueThenShouldAskDownloadsTabExtensionToSaveData() async throws { // GIVEN let expectedURL = testDirectory.appendingPathComponent(filename) let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: true) XCTAssertFalse(fileManager.fileExists(atPath: expectedURL.path)) // WHEN - sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf") sut.$savePanelDialogRequest .sink { request in request?.submit( (url: expectedURL, fileType: nil)) } .store(in: &cancellables) + _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf", + originatingURL: testOriginatingURL.appendingPathExtension("fake")) // THEN XCTAssertTrue(fileManager.fileExists(atPath: expectedURL.path)) } - @MainActor - func testWhenSaveDataAndFileExistThenURLShouldIncrementIndex() throws { + func testWhenAlwaysRequestDownloadLocationIsTrueAndNoDataProvidedAndOriginatingFileURLProvidedThenShouldAskDownloadsTabExtensionToSaveData() async throws { + // GIVEN + let expectedURL = testDirectory.appendingPathComponent(filename) + let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: true) + XCTAssertFalse(fileManager.fileExists(atPath: expectedURL.path)) + + // WHEN + sut.$savePanelDialogRequest + .sink { request in + request?.submit( (url: expectedURL, fileType: nil)) + } + .store(in: &cancellables) + _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL) + + // THEN + XCTAssertTrue(fileManager.fileExists(atPath: expectedURL.path)) + } + + func testWhenSaveDataAndFileExistThenURLShouldIncrementIndex() async throws { + // GIVEN + let destURL = testDirectory.appendingPathComponent(filename) + let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) + let expectedFilename = "Document 1.pdf" + let expectedDestURL = testDirectory.appendingPathComponent(expectedFilename) + try testData.write(to: destURL) + XCTAssertFalse(fileManager.fileExists(atPath: expectedDestURL.path)) + + // WHEN + _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf", + originatingURL: testOriginatingURL.appendingPathExtension("fake")) + + // THEN + XCTAssertTrue(fileManager.fileExists(atPath: expectedDestURL.path)) + } + + func testWhenNoDataProvidedAndOriginatingFileURLProvidedAndFileExistsThenFileShouldBeCopiedIncrementingIndex() async throws { // GIVEN let destURL = testDirectory.appendingPathComponent(filename) let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) @@ -74,21 +113,34 @@ final class DownloadsTabExtensionTests: XCTestCase { XCTAssertFalse(fileManager.fileExists(atPath: expectedDestURL.path)) // WHEN - sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf") + _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL) // THEN XCTAssertTrue(fileManager.fileExists(atPath: expectedDestURL.path)) } - @MainActor - func testWhenSaveDataAndFileDoesNotExistThenURLShouldNotIncrementIndex() throws { + func testWhenSaveDataAndFileDoesNotExistThenURLShouldNotIncrementIndex() async throws { + // GIVEN + let destURL = testDirectory.appendingPathComponent(filename) + let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) + XCTAssertFalse(fileManager.fileExists(atPath: destURL.path)) + + // WHEN + _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf", + originatingURL: testOriginatingURL.appendingPathExtension("fake")) + + // THEN + XCTAssertTrue(fileManager.fileExists(atPath: destURL.path)) + } + + func testNoDataProvidedAndOriginatingFileURLProvidedAndFileDoesNotExistThenFileShouldBeCopiedNotIncrementingIndex() async throws { // GIVEN let destURL = testDirectory.appendingPathComponent(filename) let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) XCTAssertFalse(fileManager.fileExists(atPath: destURL.path)) // WHEN - sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf") + _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL) // THEN XCTAssertTrue(fileManager.fileExists(atPath: destURL.path)) diff --git a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift index ed2ce03187..94505acb57 100644 --- a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift +++ b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift @@ -31,6 +31,7 @@ class DownloadsTabExtensionMock: NSObject, DownloadsTabExtensionProtocol { private(set) var capturedSavedDownloadData: Data? private(set) var capturedSuggestedFilename: String? private(set) var capturedMimeType: String? + private(set) var capturedOriginatingURL: URL? var savePanelDialogSubject = PassthroughSubject() @@ -40,16 +41,19 @@ class DownloadsTabExtensionMock: NSObject, DownloadsTabExtensionProtocol { savePanelDialogSubject.eraseToAnyPublisher() } - func saveWebViewContentAs(_ webView: WKWebView) { + func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) { didCallSaveWebViewContent = true capturedWebView = webView } - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) { + func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? { didCallSaveDownloadedData = true capturedSavedDownloadData = data capturedSuggestedFilename = suggestedFilename capturedMimeType = mimeType + capturedOriginatingURL = originatingURL + + return nil } func chooseDestination(suggestedFilename: String?, directoryURL: URL?, fileTypes: [UTType], callback: @escaping @MainActor (URL?, UTType?) -> Void) {} diff --git a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift index 806c139493..b569bdb58d 100644 --- a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift +++ b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift @@ -49,4 +49,10 @@ final class WKWebViewPrivateMethodsAvailabilityTests: XCTestCase { XCTAssertEqual(pagePrefs.customHeaderFields, customHeaderFields.map { [$0] }) } + func testWKPDFHUDViewClassAvailable() { + XCTAssertNotNil(WKPDFHUDViewWrapper.WKPDFHUDViewClass) + XCTAssertTrue(WKPDFHUDViewWrapper.WKPDFHUDViewClass?.instancesRespond(to: WKPDFHUDViewWrapper.performActionForControlSelector) == true) + XCTAssertTrue(WKPDFHUDViewWrapper.WKPDFHUDViewClass?.instancesRespond(to: WKPDFHUDViewWrapper.setVisibleSelector) == true) + } + } From 27f5ef95b5a56745ad437639598a4d09a7abf84f Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 23 Feb 2024 18:01:33 +0600 Subject: [PATCH 02/23] fix getting active tab in a Popup window --- DuckDuckGo/Menus/MainMenuActions.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index eb9ff78c8c..1f2fc099e6 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -265,7 +265,16 @@ extension MainViewController { /// Finds currently active Tab even if it‘s playing a Full Screen video private func getActiveTabAndIndex() -> (tab: Tab, index: TabIndex)? { - guard let tab = WindowControllersManager.shared.lastKeyMainWindowController?.activeTab else { + var tab: Tab? { + if let window = self.view.window, + window.isKeyWindow, + let mainWindowController = window.nextResponder as? MainWindowController, + let tab = mainWindowController.activeTab { + return tab + } + return WindowControllersManager.shared.lastKeyMainWindowController?.activeTab + } + guard let tab else { assertionFailure("Could not get currently active Tab") return nil } From 6f9e9676eb95a8788a6282e0e56b607e4fe7040d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 23 Feb 2024 18:26:21 +0600 Subject: [PATCH 03/23] fix tests --- .../DownloadListCoordinatorTests.swift | 10 ++-- .../FileDownloadManagerTests.swift | 46 +++++++++++++++---- .../FileDownload/Helpers/WKDownloadMock.swift | 4 ++ UnitTests/Tab/Model/TabTests.swift | 8 +++- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift index 4cb67403f7..b131448893 100644 --- a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift +++ b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift @@ -63,7 +63,7 @@ final class DownloadListCoordinatorTests: XCTestCase { func setUpCoordinatorAndAddDownload(isBurner: Bool = false) -> (WKDownloadMock, WebKitDownloadTask, UUID) { setUpCoordinator() - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) let task = WebKitDownloadTask(download: download, promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: isBurner) let e = expectation(description: "download added") @@ -144,7 +144,7 @@ final class DownloadListCoordinatorTests: XCTestCase { func testWhenDownloadAddedThenDownloadItemIsPublished() { setUpCoordinator() - let task = WebKitDownloadTask(download: WKDownloadMock(), promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: false) + let task = WebKitDownloadTask(download: WKDownloadMock(url: .duckDuckGo), promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: false) let e = expectation(description: "download added") let c = coordinator.updates.sink { [coordinator] (kind, item) in @@ -245,7 +245,7 @@ final class DownloadListCoordinatorTests: XCTestCase { webView.resumeDownloadBlock = { data in resumeCalled.fulfill() XCTAssertEqual(data, .resumeData) - return WKDownloadMock() + return WKDownloadMock(url: .duckDuckGo) } webView.startDownloadBlock = { _ in XCTFail("unexpected start call") @@ -298,7 +298,7 @@ final class DownloadListCoordinatorTests: XCTestCase { webView.resumeDownloadBlock = { data in resumeCalled.fulfill() XCTAssertEqual(data, .resumeData) - return WKDownloadMock() + return WKDownloadMock(url: .duckDuckGo) } webView.startDownloadBlock = { _ in XCTFail("unexpected start call") @@ -354,7 +354,7 @@ final class DownloadListCoordinatorTests: XCTestCase { webView.startDownloadBlock = { request in startCalled.fulfill() XCTAssertEqual(request?.url, item.url) - return WKDownloadMock() + return WKDownloadMock(url: .duckDuckGo) } let downloadAdded = expectation(description: "download addeed") diff --git a/UnitTests/FileDownload/FileDownloadManagerTests.swift b/UnitTests/FileDownload/FileDownloadManagerTests.swift index 547646db35..02cc8b0c95 100644 --- a/UnitTests/FileDownload/FileDownloadManagerTests.swift +++ b/UnitTests/FileDownload/FileDownloadManagerTests.swift @@ -56,6 +56,7 @@ final class FileDownloadManagerTests: XCTestCase { FileManager.restoreUrlsForIn() self.chooseDestination = nil self.fileIconFlyAnimationOriginalRect = nil + preferences.alwaysRequestDownloadLocation = false } func testWhenDownloadIsAddedThenItsPublished() { @@ -64,7 +65,7 @@ final class FileDownloadManagerTests: XCTestCase { e.fulfill() } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto) withExtendedLifetime(cancellable) { @@ -78,7 +79,7 @@ final class FileDownloadManagerTests: XCTestCase { e.fulfill() } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto) struct TestError: Error {} @@ -96,7 +97,7 @@ final class FileDownloadManagerTests: XCTestCase { e.fulfill() } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto) download.delegate?.downloadDidFinish!(download.asWKDownload()) @@ -122,7 +123,7 @@ final class FileDownloadManagerTests: XCTestCase { callback(nil, nil) } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .prompt) let url = URL(string: "https://duckduckgo.com/somefile.html")! @@ -155,7 +156,7 @@ final class FileDownloadManagerTests: XCTestCase { callback(localURL, .html) } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) let url = URL(string: "https://duckduckgo.com/somefile.html")! @@ -177,7 +178,7 @@ final class FileDownloadManagerTests: XCTestCase { fm.createFile(atPath: localURL.path, contents: nil, attributes: nil) XCTAssertTrue(fm.fileExists(atPath: localURL.path)) - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .prompt) self.chooseDestination = { _, _, _, callback in callback(localURL, nil) @@ -194,11 +195,38 @@ final class FileDownloadManagerTests: XCTestCase { XCTAssertFalse(fm.fileExists(atPath: localURL.path)) } + func testWhenDownloadingLocalFileThenLocationChooserIsCalled() { + let downloadsURL = fm.temporaryDirectory + preferences.selectedDownloadLocation = downloadsURL + + let download = WKDownloadMock(url: URL(fileURLWithPath: "/some/path")) + + let localURL = downloadsURL.appendingPathComponent(testFile) + let e1 = expectation(description: "chooseDestinationCallback called") + self.chooseDestination = { suggestedFilename, directoryURL, fileTypes, callback in + dispatchPrecondition(condition: .onQueue(.main)) + XCTAssertEqual(directoryURL, downloadsURL) + e1.fulfill() + + callback(localURL, .html) + } + + dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) + + let e2 = expectation(description: "WKDownload called") + download.delegate?.download(download.asWKDownload(), decideDestinationUsing: response, suggestedFilename: testFile) { [testFile] url in + XCTAssertEqual(url, downloadsURL.appendingPathComponent(testFile).appendingPathExtension(WebKitDownloadTask.downloadExtension)) + e2.fulfill() + } + + waitForExpectations(timeout: 0.3) + } + func testWhenNotRequiredByPreferencesThenDefaultDownloadLocationIsChosen() { let downloadsURL = fm.temporaryDirectory preferences.selectedDownloadLocation = downloadsURL - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) self.chooseDestination = { _, _, _, _ in XCTFail("Unpected chooseDestination call") @@ -217,7 +245,7 @@ final class FileDownloadManagerTests: XCTestCase { let downloadsURL = fm.temporaryDirectory preferences.selectedDownloadLocation = downloadsURL - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) self.chooseDestination = { _, _, _, _ in XCTFail("Unpected chooseDestination call") @@ -240,7 +268,7 @@ final class FileDownloadManagerTests: XCTestCase { [URL(fileURLWithPath: "/")] } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) let e = expectation(description: "WKDownload called") diff --git a/UnitTests/FileDownload/Helpers/WKDownloadMock.swift b/UnitTests/FileDownload/Helpers/WKDownloadMock.swift index a00a8a1bf4..acff9884a1 100644 --- a/UnitTests/FileDownload/Helpers/WKDownloadMock.swift +++ b/UnitTests/FileDownload/Helpers/WKDownloadMock.swift @@ -26,6 +26,10 @@ final class WKDownloadMock: NSObject, WebKitDownload, ProgressReporting { var progress = Progress() weak var delegate: WKDownloadDelegate? + init(url: URL) { + self.originalRequest = URLRequest(url: url) + } + var cancelBlock: (() -> Void)? @objc func cancel() { cancelBlock?() diff --git a/UnitTests/Tab/Model/TabTests.swift b/UnitTests/Tab/Model/TabTests.swift index faf05897e4..ed15925879 100644 --- a/UnitTests/Tab/Model/TabTests.swift +++ b/UnitTests/Tab/Model/TabTests.swift @@ -120,11 +120,15 @@ final class TabTests: XCTestCase { DownloadsPreferences().alwaysRequestDownloadLocation = true tab.webView(WebViewMock(), saveDataToFile: Data(), suggestedFilename: "anything", mimeType: "application/pdf", originatingURL: .duckDuckGo) var expectedDialog: Tab.UserDialog? + let expectation = expectation(description: "savePanelDialog published") tab.downloads?.savePanelDialogPublisher.sink(receiveValue: { userDialog in - expectedDialog = userDialog + if let userDialog { + expectation.fulfill() + expectedDialog = userDialog + } }).store(in: &cancellables) - XCTAssertNotNil(expectedDialog) + waitForExpectations(timeout: 1) // WHEN tab.url = .duckDuckGoMorePrivacyInfo From 66b8402b5a1c866397f14fe1ec536c444b02ce9f Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 23 Feb 2024 23:22:37 +0600 Subject: [PATCH 04/23] fix tests --- .../FileDownload/Model/FileDownloadManager.swift | 2 +- .../Helpers/DownloadsTabExtensionMock.swift | 1 + UnitTests/FileDownload/Tab+WKUIDelegateTests.swift | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift index 7e8aacc576..eb271ab5a6 100644 --- a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift +++ b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift @@ -87,7 +87,7 @@ final class FileDownloadManager: FileDownloadManagerProtocol { switch self { case .prompt: return true case .preset: return false - case .auto: return (url?.isFileURL ?? true || url?.isLocalURL ?? true) // always prompt when "downloading" a local file + case .auto: return url?.isFileURL ?? true // always prompt when "downloading" a local file } } } diff --git a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift index 94505acb57..f2e14bf8d2 100644 --- a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift +++ b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift @@ -27,6 +27,7 @@ class DownloadsTabExtensionMock: NSObject, DownloadsTabExtensionProtocol { private(set) var didCallSaveWebViewContent = false private(set) var capturedWebView: WKWebView? + @Published private(set) var didCallSaveDownloadedData = false private(set) var capturedSavedDownloadData: Data? private(set) var capturedSuggestedFilename: String? diff --git a/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift b/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift index 7fe993f1e2..c0f52330be 100644 --- a/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift +++ b/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift @@ -57,13 +57,22 @@ final class TabWKUIDelegateTests: XCTestCase { XCTAssertNil(downloadExtensionMock.capturedSavedDownloadData) XCTAssertNil(downloadExtensionMock.capturedMimeType) + let eDidCallSaveDownloadedData = expectation(description: "didCallSaveDownloadedData") + let c = downloadExtensionMock.$didCallSaveDownloadedData.sink { didCall in + if didCall { + eDidCallSaveDownloadedData.fulfill() + } + } + // WHEN sut.webView(WKWebView(), saveDataToFile: testData, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: originatingURL) // THEN - XCTAssertTrue(downloadExtensionMock.didCallSaveDownloadedData) + waitForExpectations(timeout: 1) XCTAssertEqual(downloadExtensionMock.capturedSavedDownloadData, testData) XCTAssertNotNil(downloadExtensionMock.capturedMimeType, "application/pdf") + + withExtendedLifetime(c) {} } } From e8ee236b98b440358fbe76714a36462e71d84acd Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 26 Feb 2024 11:49:25 +0600 Subject: [PATCH 05/23] fix resetting savePanelDialogRequest --- DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index 248665c18b..64071f5119 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -43,7 +43,7 @@ final class DownloadsTabExtension: NSObject { @Published private(set) var savePanelDialogRequest: SavePanelDialogRequest? { willSet { - newValue?.addCompletionHandler { [weak self, weak savePanelDialogRequest] _ in + newValue?.addCompletionHandler { [weak self, weak savePanelDialogRequest=newValue] _ in if let self, let savePanelDialogRequest, self.savePanelDialogRequest === savePanelDialogRequest { @@ -241,8 +241,7 @@ extension DownloadsTabExtension: DownloadTaskDelegate { @MainActor func chooseDestination(suggestedFilename: String?, directoryURL: URL?, fileTypes: [UTType], callback: @escaping @MainActor (URL?, UTType?) -> Void) { - savePanelDialogRequest = SavePanelDialogRequest(SavePanelParameters(suggestedFilename: suggestedFilename, fileTypes: fileTypes)) { [weak self] result in - self?.savePanelDialogRequest = nil + savePanelDialogRequest = SavePanelDialogRequest(SavePanelParameters(suggestedFilename: suggestedFilename, fileTypes: fileTypes)) { result in guard case let .success(.some( (url: url, fileType: fileType) )) = result else { callback(nil, nil) return From 2dc38b7deba6b8ae1d66165071364fb1b8bfb1e9 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 26 Feb 2024 11:54:50 +0600 Subject: [PATCH 06/23] cleanup PDF after printing --- DuckDuckGo/Tab/Model/Tab+UIDelegate.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift index 482d8b71f1..3de0df0c9b 100644 --- a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift +++ b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift @@ -348,7 +348,9 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { return } - self.runPrintOperation(printOperation) + self.runPrintOperation(printOperation) { _ in + try? FileManager.default.removeItem(at: url) + } } saveWebContent(pdfHUD: pdfHUD, location: .temporary) return From 84de49b33f40dd63eea206174a469c8b70038221 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 26 Feb 2024 14:53:45 +0600 Subject: [PATCH 07/23] improve naming, add comments --- .../Extensions/WKPDFHUDViewWrapper.swift | 10 ++++-- .../Extensions/WKWebViewExtension.swift | 2 +- DuckDuckGo/Menus/MainMenuActions.swift | 36 ++++++++----------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift index e93ea14221..ef4919b1fd 100644 --- a/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift +++ b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift @@ -41,6 +41,9 @@ struct WKPDFHUDViewWrapper { } } + /// Create a wrapper over the PDF HUD view validating its class is `WKPDFHUDView` + /// - parameter view: the WKPDFHUDView to wrap + /// - returns nil if the view init?(view: NSView) { guard type(of: view) == Self.WKPDFHUDViewClass else { return nil } @@ -51,7 +54,10 @@ struct WKPDFHUDViewWrapper { self.hudView = view } - init?(webView: WKWebView, location: NSPoint? = nil) { + /// Find WebView‘s PDF HUD view at a clicked point + /// + /// Used to get PDF controls view of a clicked WebView frame for `Print…` and `Save As…` PDF context menu commands + static func getPdfHudView(in webView: WKWebView, at location: NSPoint? = nil) -> Self? { guard let hudView = webView.subviews.last(where: { type(of: $0) == Self.WKPDFHUDViewClass && $0.frame.contains(location ?? $0.frame.origin) }) else { #if DEBUG Task { @@ -62,7 +68,7 @@ struct WKPDFHUDViewWrapper { #endif return nil } - self.init(view: hudView) + return self.init(view: hudView) } func savePDF() { diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index f581f31243..fcd19de900 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -326,7 +326,7 @@ extension WKWebView { } func hudView(at point: NSPoint? = nil) -> WKPDFHUDViewWrapper? { - WKPDFHUDViewWrapper(webView: self, location: point) + WKPDFHUDViewWrapper.getPdfHudView(in: self, at: point) } func savePDF(_ pdfHUD: WKPDFHUDViewWrapper? = nil) -> Bool { diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 1f2fc099e6..83f3c29b39 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -630,33 +630,14 @@ extension MainViewController { // MARK: - Printing @objc func printWebView(_ sender: Any?) { - var pdfHUD: WKPDFHUDViewWrapper? - // if saving a PDF (may be from a frame) - if let menuItem = sender as? NSMenuItem, - let representedObject = menuItem.representedObject { - - pdfHUD = representedObject as? WKPDFHUDViewWrapper ?? { - assertionFailure("Unexpected Save As menu item represented object: \(representedObject)") - return nil - }() - } - + let pdfHUD = (sender as? NSMenuItem)?.pdfHudRepresentedObject // if printing a PDF (may be from a frame context menu) getActiveTabAndIndex()?.tab.print(pdfHUD: pdfHUD) } // MARK: - Saving @objc func saveAs(_ sender: Any) { - var pdfHUD: WKPDFHUDViewWrapper? - // if saving a PDF (may be from a frame) - if let menuItem = sender as? NSMenuItem, - let representedObject = menuItem.representedObject { - - pdfHUD = representedObject as? WKPDFHUDViewWrapper ?? { - assertionFailure("Unexpected Save As menu item represented object: \(representedObject)") - return nil - }() - } + let pdfHUD = (sender as? NSMenuItem)?.pdfHudRepresentedObject // if saving a PDF (may be from a frame context menu) getActiveTabAndIndex()?.tab.saveWebContent(pdfHUD: pdfHUD, location: .prompt) } @@ -1047,3 +1028,16 @@ extension AppDelegate: PrivacyDashboardViewControllerSizeDelegate { privacyDashboardWindow?.setFrame(NSRect(origin: .zero, size: size), display: true, animate: true) } } + +private extension NSMenuItem { + + var pdfHudRepresentedObject: WKPDFHUDViewWrapper? { + guard let representedObject = representedObject else { return nil } + + return representedObject as? WKPDFHUDViewWrapper ?? { + assertionFailure("Unexpected SaveAs/Print menu item represented object: \(representedObject)") + return nil + }() + } + +} From 30167968bc5bd86f446cc242a13f8aa293f0402f Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 26 Feb 2024 14:54:07 +0600 Subject: [PATCH 08/23] fix assertion for context menu shown in background --- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 83f3c29b39..5903854d99 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -266,8 +266,8 @@ extension MainViewController { /// Finds currently active Tab even if it‘s playing a Full Screen video private func getActiveTabAndIndex() -> (tab: Tab, index: TabIndex)? { var tab: Tab? { + // popup windows don‘t get to lastKeyMainWindowController so try getting their WindowController directly fron a key window if let window = self.view.window, - window.isKeyWindow, let mainWindowController = window.nextResponder as? MainWindowController, let tab = mainWindowController.activeTab { return tab From c1a5c80fe2dfae921f6bb59482da7bc0b418d580 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 26 Feb 2024 14:54:21 +0600 Subject: [PATCH 09/23] rollback InfoPlist.xcstrings --- DuckDuckGo/InfoPlist.xcstrings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index bfcde1d533..4d7c2d94c0 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -75,4 +75,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file From 9334b1b4e01660eaf7bef5115f78749d22282862 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 26 Feb 2024 15:02:35 +0600 Subject: [PATCH 10/23] Position Print&SaveAs items below Open with Preview --- DuckDuckGo/MainWindow/MainView.swift | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/MainWindow/MainView.swift b/DuckDuckGo/MainWindow/MainView.swift index a61376a296..4bea96ac54 100644 --- a/DuckDuckGo/MainWindow/MainView.swift +++ b/DuckDuckGo/MainWindow/MainView.swift @@ -144,18 +144,23 @@ final class MainView: NSView { return nil } - // Insert Save As… and Print… items - let copyItemIdx = menu.indexOfItem(withTitle: UserText.copy) - let separatorAfterCopyItemIdx = ( - menu.items.indices.contains(copyItemIdx) - ? (copyItemIdx.. 0 { + // 2. find separator below `Copy` + let separatorIdx = (idxAfterCopy.. Date: Mon, 26 Feb 2024 15:02:48 +0600 Subject: [PATCH 11/23] add tests --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++ IntegrationTests/Tab/TabContentTests.swift | 190 +++++++++++++++++++++ IntegrationTests/Tab/empty.pdf | Bin 0 -> 3833 bytes 3 files changed, 202 insertions(+) create mode 100644 IntegrationTests/Tab/TabContentTests.swift create mode 100644 IntegrationTests/Tab/empty.pdf diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6be858051a..4743b3df77 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3031,6 +3031,10 @@ B6EC37FC29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; }; B6EC37FD29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; }; B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = B6EC37FE29B8D915001ACE79 /* Configuration */; }; + B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; + B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; + B6EEDD7F2B8C6B8B00637EBC /* empty.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B6EEDD792B8C65F000637EBC /* empty.pdf */; }; + B6EEDD802B8C6B8C00637EBC /* empty.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B6EEDD792B8C65F000637EBC /* empty.pdf */; }; B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F41030264D2B23003DA42C /* ProgressExtension.swift */; }; B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; }; @@ -4421,6 +4425,8 @@ B6EC37EA29B5DA2A001ACE79 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; B6EC37FA29B6447F001ACE79 /* TestsServer.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TestsServer.xcconfig; sourceTree = ""; }; B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = ""; }; + B6EEDD792B8C65F000637EBC /* empty.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = empty.pdf; sourceTree = ""; }; + B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabContentTests.swift; sourceTree = ""; }; B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = ""; }; B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; }; @@ -7885,8 +7891,10 @@ B644B43C29D56811003FA9AB /* Tab */ = { isa = PBXGroup; children = ( + B6EEDD792B8C65F000637EBC /* empty.pdf */, B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */, B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */, + B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */, ); path = Tab; sourceTree = ""; @@ -9029,6 +9037,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B6EEDD802B8C6B8C00637EBC /* empty.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9043,6 +9052,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B6EEDD7F2B8C6B8B00637EBC /* empty.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10560,6 +10570,7 @@ B62A234129C41D4400D22475 /* HistoryIntegrationTests.swift in Sources */, B603973929BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift in Sources */, B644B43E29D5682B003FA9AB /* SearchNonexistentDomainTests.swift in Sources */, + B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */, 3706FEA5293F662100E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, B603973D29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */, B60C6F8729B1CAB2007BFAA8 /* TestRunHelper.swift in Sources */, @@ -10601,6 +10612,7 @@ 4B1AD92125FC474E00261379 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, B62A234029C41D4400D22475 /* HistoryIntegrationTests.swift in Sources */, B603973829BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift in Sources */, + B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */, B644B43D29D56829003FA9AB /* SearchNonexistentDomainTests.swift in Sources */, B603973C29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */, B60C6F8629B1CAB0007BFAA8 /* TestRunHelper.swift in Sources */, diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift new file mode 100644 index 0000000000..36551ac44c --- /dev/null +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -0,0 +1,190 @@ +// +// TabContentTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Common +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@available(macOS 12.0, *) +@MainActor +class TabContentTests: XCTestCase { + + var window: NSWindow! + + var mainViewController: MainViewController { + (window.contentViewController as! MainViewController) + } + + var tabViewModel: TabViewModel { + mainViewController.browserTabViewController.tabViewModel! + } + + @MainActor + override func setUp() async throws { + } + + @MainActor + override func tearDown() async throws { + window?.close() + window = nil + } + + @MainActor + func testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "empty", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + let point = tab.webView.convert(tab.webView.bounds.center, to: nil) + + NSApp.activate(ignoringOtherApps: true) + let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown, + location: point, + modifierFlags: [], + timestamp: CACurrentMediaTime(), + windowNumber: window.windowNumber, + context: nil, + eventNumber: -22966, + clickCount: 1, + pressure: 1)! + + // wait for context menu to appear + let menuWindowPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in + print(NSApp.windows.map(\.className)) + return NSApp.windows.first(where: { + $0.className == "NSPopupMenuWindow" + }) + }.timeout(1).first().promise() + + // right-click + NSApp.sendEvent(mouseDown) + let menuWindow = try await menuWindowPromise.value + + // find Print, Save As + let menuItems = menuWindow.contentView?.recursivelyFindMenuItemViews() + let printMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.printMenuItem }) + let saveAsMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.mainMenuFileSaveAs }) + XCTAssertNotNil(printMenuItem) + XCTAssertNotNil(saveAsMenuItem) + + // wait for print dialog to appear + let printDialogPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in + self.window.sheets.first + }.timeout(1).first().promise() + + // Click Print… + printMenuItem?.menuItem.accessibilityPerformPress() + + let printDialog = try await printDialogPromise.value + XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "…")) + } + + @MainActor + func testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "empty", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + let point = tab.webView.convert(tab.webView.bounds.center, to: nil) + + NSApp.activate(ignoringOtherApps: true) + let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown, + location: point, + modifierFlags: [], + timestamp: CACurrentMediaTime(), + windowNumber: window.windowNumber, + context: nil, + eventNumber: -22966, + clickCount: 1, + pressure: 1)! + + // wait for context menu to appear + let menuWindowPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in + print(NSApp.windows.map(\.className)) + return NSApp.windows.first(where: { + $0.className == "NSPopupMenuWindow" + }) + }.timeout(5).first().promise() + + // right-click + NSApp.sendEvent(mouseDown) + let menuWindow = try await menuWindowPromise.value + + // find Print, Save As + let menuItems = menuWindow.contentView?.recursivelyFindMenuItemViews() + let printMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.printMenuItem }) + let saveAsMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.mainMenuFileSaveAs }) + XCTAssertNotNil(printMenuItem) + XCTAssertNotNil(saveAsMenuItem) + + // wait for save dialog to appear + let saveDialogPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in + self.window.sheets.first as? NSSavePanel + }.timeout(5).first().promise() + + // Click Save As… + saveAsMenuItem?.menuItem.accessibilityPerformPress() + + let saveDialog = try await saveDialogPromise.value + guard let url = saveDialog.url else { + XCTFail("no Save Dialog url") + return + } + try? FileManager.default.removeItem(at: url) + + // wait until file is saved + let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in + FileManager.default.fileExists(atPath: url.path) + }.timeout(5).first().promise() + defer { + try? FileManager.default.removeItem(at: url) + } + + window.endSheet(saveDialog, returnCode: .OK) + + _=try await fileSavedPromise.value + try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) + } + +} + +private extension NSView { + + func recursivelyFindMenuItemViews() -> [(view: NSView, menuItem: NSMenuItem)] { + var result = [(view: NSView, menuItem: NSMenuItem)]() + for subview in subviews { + guard subview.className == "NSContextMenuItemView" else { + result.append(contentsOf: subview.recursivelyFindMenuItemViews()) + continue + } + let menuItem = subview.value(forKey: "menuItem") as! NSMenuItem + result.append((subview, menuItem)) + } + return result + } + +} diff --git a/IntegrationTests/Tab/empty.pdf b/IntegrationTests/Tab/empty.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d5b4c5c817d9440d1e45ae42d42a997b303bf292 GIT binary patch literal 3833 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 BakBsb literal 0 HcmV?d00001 From bc305a1d24b44cfd49b5d78aded17f1b7e6e619d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 27 Feb 2024 12:57:39 +0600 Subject: [PATCH 12/23] Extend test timeouts --- IntegrationTests/Tab/TabContentTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index 36551ac44c..e6a4e1847c 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -74,7 +74,7 @@ class TabContentTests: XCTestCase { return NSApp.windows.first(where: { $0.className == "NSPopupMenuWindow" }) - }.timeout(1).first().promise() + }.timeout(5).first().promise() // right-click NSApp.sendEvent(mouseDown) @@ -90,7 +90,7 @@ class TabContentTests: XCTestCase { // wait for print dialog to appear let printDialogPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in self.window.sheets.first - }.timeout(1).first().promise() + }.timeout(5).first().promise() // Click Print… printMenuItem?.menuItem.accessibilityPerformPress() From d5ff3a57543f89f5aab40d6674c51067522bf070 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 27 Feb 2024 21:41:25 +0600 Subject: [PATCH 13/23] disabled context menu tests - would it fix the tests?! --- IntegrationTests/Tab/TabContentTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index e6a4e1847c..cec2c1d41d 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -46,7 +46,7 @@ class TabContentTests: XCTestCase { } @MainActor - func testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws { + func disabled_testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws { let pdfUrl = Bundle(for: Self.self).url(forResource: "empty", withExtension: "pdf")! // open Tab with PDF let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) @@ -100,7 +100,7 @@ class TabContentTests: XCTestCase { } @MainActor - func testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws { + func disabled_testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws { let pdfUrl = Bundle(for: Self.self).url(forResource: "empty", withExtension: "pdf")! // open Tab with PDF let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) From b1a1eaf740eb821417cfeb29b44cf21e449aa1ea Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 29 Feb 2024 14:20:35 +0600 Subject: [PATCH 14/23] refactor 1 test --- IntegrationTests/Tab/TabContentTests.swift | 43 ++++++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index cec2c1d41d..111d8d48ca 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -45,8 +45,10 @@ class TabContentTests: XCTestCase { window = nil } + // MARK: - Tests + @MainActor - func disabled_testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws { + func testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws { let pdfUrl = Bundle(for: Self.self).url(forResource: "empty", withExtension: "pdf")! // open Tab with PDF let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) @@ -69,16 +71,23 @@ class TabContentTests: XCTestCase { pressure: 1)! // wait for context menu to appear - let menuWindowPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in - print(NSApp.windows.map(\.className)) - return NSApp.windows.first(where: { - $0.className == "NSPopupMenuWindow" - }) - }.timeout(5).first().promise() + let eMenuShown = expectation(description: "menu shown") + let getMenuWindow = Task { + while true { + if let window = NSApp.windows.first(where: { $0.className == "NSPopupMenuWindow" }) { + eMenuShown.fulfill() + return window + } + try await Task.sleep(interval: 0.01) + } + } // right-click NSApp.sendEvent(mouseDown) - let menuWindow = try await menuWindowPromise.value + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eMenuShown], timeout: 5) { + getMenuWindow.cancel() + } + let menuWindow = try await getMenuWindow.value // find Print, Save As let menuItems = menuWindow.contentView?.recursivelyFindMenuItemViews() @@ -88,14 +97,24 @@ class TabContentTests: XCTestCase { XCTAssertNotNil(saveAsMenuItem) // wait for print dialog to appear - let printDialogPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in - self.window.sheets.first - }.timeout(5).first().promise() + let ePrintDialogShown = expectation(description: "Print dialog shown") + let getPrintDialog = Task { + while true { + if let sheet = self.window.sheets.first { + ePrintDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } // Click Print… printMenuItem?.menuItem.accessibilityPerformPress() + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) { + getPrintDialog.cancel() + } + let printDialog = try await getPrintDialog.value - let printDialog = try await printDialogPromise.value XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "…")) } From 0f26ab8cc9e2063f8848fc780c527062202c3d0f Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 29 Feb 2024 15:56:05 +0600 Subject: [PATCH 15/23] fixing hanging test --- DuckDuckGo/MainWindow/MainView.swift | 1 + IntegrationTests/Tab/TabContentTests.swift | 133 +++++++++++---------- 2 files changed, 72 insertions(+), 62 deletions(-) diff --git a/DuckDuckGo/MainWindow/MainView.swift b/DuckDuckGo/MainWindow/MainView.swift index 41d0d77405..f8e09d70ea 100644 --- a/DuckDuckGo/MainWindow/MainView.swift +++ b/DuckDuckGo/MainWindow/MainView.swift @@ -108,6 +108,7 @@ final class MainView: NSView { override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { setupSearchContextMenuItem(menu: menu) setupSaveAsAndPrintMenuItems(menu: menu, with: event) + super.willOpenMenu(menu, with: event) } private func setupSearchContextMenuItem(menu: NSMenu) { diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index 111d8d48ca..2f351b0e77 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -43,6 +43,7 @@ class TabContentTests: XCTestCase { override func tearDown() async throws { window?.close() window = nil + NSView.swizzleWillOpenMenu(with: nil) } // MARK: - Tests @@ -72,28 +73,23 @@ class TabContentTests: XCTestCase { // wait for context menu to appear let eMenuShown = expectation(description: "menu shown") - let getMenuWindow = Task { - while true { - if let window = NSApp.windows.first(where: { $0.className == "NSPopupMenuWindow" }) { - eMenuShown.fulfill() - return window - } - try await Task.sleep(interval: 0.01) - } + var menuItems = [NSMenuItem]() + NSView.swizzleWillOpenMenu { menu, event in + menuItems = menu.items + menu.removeAllItems() + eMenuShown.fulfill() } // right-click - NSApp.sendEvent(mouseDown) - if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eMenuShown], timeout: 5) { - getMenuWindow.cancel() - } - let menuWindow = try await getMenuWindow.value + window.sendEvent(mouseDown) + await fulfillment(of: [eMenuShown]) // find Print, Save As - let menuItems = menuWindow.contentView?.recursivelyFindMenuItemViews() - let printMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.printMenuItem }) - let saveAsMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.mainMenuFileSaveAs }) - XCTAssertNotNil(printMenuItem) + guard let printMenuItem = menuItems.first(where: { $0.title == UserText.printMenuItem }) else { + XCTFail("No print menu item") + return + } + let saveAsMenuItem = menuItems.first(where: { $0.title == UserText.mainMenuFileSaveAs }) XCTAssertNotNil(saveAsMenuItem) // wait for print dialog to appear @@ -109,7 +105,7 @@ class TabContentTests: XCTestCase { } // Click Print… - printMenuItem?.menuItem.accessibilityPerformPress() + printMenuItem.accessibilityPerformPress() if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) { getPrintDialog.cancel() } @@ -150,60 +146,73 @@ class TabContentTests: XCTestCase { }.timeout(5).first().promise() // right-click - NSApp.sendEvent(mouseDown) + window.sendEvent(mouseDown) let menuWindow = try await menuWindowPromise.value // find Print, Save As - let menuItems = menuWindow.contentView?.recursivelyFindMenuItemViews() - let printMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.printMenuItem }) - let saveAsMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.mainMenuFileSaveAs }) - XCTAssertNotNil(printMenuItem) - XCTAssertNotNil(saveAsMenuItem) - - // wait for save dialog to appear - let saveDialogPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in - self.window.sheets.first as? NSSavePanel - }.timeout(5).first().promise() - - // Click Save As… - saveAsMenuItem?.menuItem.accessibilityPerformPress() - - let saveDialog = try await saveDialogPromise.value - guard let url = saveDialog.url else { - XCTFail("no Save Dialog url") - return - } - try? FileManager.default.removeItem(at: url) - - // wait until file is saved - let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in - FileManager.default.fileExists(atPath: url.path) - }.timeout(5).first().promise() - defer { - try? FileManager.default.removeItem(at: url) - } - - window.endSheet(saveDialog, returnCode: .OK) - - _=try await fileSavedPromise.value - try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) +// let menuItems = menuWindow.contentView?.recursivelyFindMenuItemViews() +// let printMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.printMenuItem }) +// let saveAsMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.mainMenuFileSaveAs }) +// XCTAssertNotNil(printMenuItem) +// XCTAssertNotNil(saveAsMenuItem) +// +// // wait for save dialog to appear +// let saveDialogPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in +// self.window.sheets.first as? NSSavePanel +// }.timeout(5).first().promise() +// +// // Click Save As… +// saveAsMenuItem?.menuItem.accessibilityPerformPress() +// +// let saveDialog = try await saveDialogPromise.value +// guard let url = saveDialog.url else { +// XCTFail("no Save Dialog url") +// return +// } +// try? FileManager.default.removeItem(at: url) +// +// // wait until file is saved +// let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in +// FileManager.default.fileExists(atPath: url.path) +// }.timeout(5).first().promise() +// defer { +// try? FileManager.default.removeItem(at: url) +// } +// +// window.endSheet(saveDialog, returnCode: .OK) +// +// _=try await fileSavedPromise.value +// try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) } } private extension NSView { - func recursivelyFindMenuItemViews() -> [(view: NSView, menuItem: NSMenuItem)] { - var result = [(view: NSView, menuItem: NSMenuItem)]() - for subview in subviews { - guard subview.className == "NSContextMenuItemView" else { - result.append(contentsOf: subview.recursivelyFindMenuItemViews()) - continue - } - let menuItem = subview.value(forKey: "menuItem") as! NSMenuItem - result.append((subview, menuItem)) + private static var willOpenMenuWithEvent: ((NSMenu, NSEvent) -> Void)? + + private static let originalWillOpenMenu = { + class_getInstanceMethod(NSView.self, #selector(NSView.willOpenMenu))! + }() + private static let swizzledWillOpenMenu = { + class_getInstanceMethod(NSView.self, #selector(NSView.swizzled_willOpenMenu))! + }() + private static let swizzleWillOpenMenuOnce: Void = { + method_exchangeImplementations(originalWillOpenMenu, swizzledWillOpenMenu) + }() + + static func swizzleWillOpenMenu(with willOpenMenuWithEvent: ((NSMenu, NSEvent) -> Void)?) { + _=swizzleWillOpenMenuOnce + self.willOpenMenuWithEvent = willOpenMenuWithEvent + } + + + @objc dynamic func swizzled_willOpenMenu(_ menu: NSMenu, with event: NSEvent) { + if let willOpenMenuWithEvent = Self.willOpenMenuWithEvent { + willOpenMenuWithEvent(menu, event) + } else { + self.swizzled_willOpenMenu(menu, with: event) // call original } - return result } } From 54a5f454a1a40e1c6e9c26de0afd8cea20a6e99f Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 29 Feb 2024 16:45:38 +0600 Subject: [PATCH 16/23] add MainActor --- IntegrationTests/Tab/TabContentTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index 2f351b0e77..86602a1504 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -94,7 +94,7 @@ class TabContentTests: XCTestCase { // wait for print dialog to appear let ePrintDialogShown = expectation(description: "Print dialog shown") - let getPrintDialog = Task { + let getPrintDialog = Task { @MainActor in while true { if let sheet = self.window.sheets.first { ePrintDialogShown.fulfill() From 529e332a2fb8985b4231261460e5558d6a7fe68d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 29 Feb 2024 16:55:26 +0600 Subject: [PATCH 17/23] fixing the test --- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- IntegrationTests/Tab/TabContentTests.swift | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 2d400f60d4..ae75667cd0 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -1029,7 +1029,7 @@ extension AppDelegate: PrivacyDashboardViewControllerSizeDelegate { } } -private extension NSMenuItem { +extension NSMenuItem { var pdfHudRepresentedObject: WKPDFHUDViewWrapper? { guard let representedObject = representedObject else { return nil } diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index 86602a1504..1d3508fa57 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -60,7 +60,6 @@ class TabContentTests: XCTestCase { let point = tab.webView.convert(tab.webView.bounds.center, to: nil) - NSApp.activate(ignoringOtherApps: true) let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown, location: point, modifierFlags: [], @@ -81,6 +80,7 @@ class TabContentTests: XCTestCase { } // right-click + NSApp.activate(ignoringOtherApps: true) window.sendEvent(mouseDown) await fulfillment(of: [eMenuShown]) @@ -104,12 +104,20 @@ class TabContentTests: XCTestCase { } } + XCTAssertNotNil(printMenuItem.action) + XCTAssertNotNil(printMenuItem.pdfHudRepresentedObject) + // Click Print… - printMenuItem.accessibilityPerformPress() + _=printMenuItem.action.map { action in + NSApp.sendAction(action, to: printMenuItem.target, from: printMenuItem) + } if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) { getPrintDialog.cancel() } let printDialog = try await getPrintDialog.value + defer { + window.endSheet(printDialog, returnCode: .cancel) + } XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "…")) } From c4af1412c3677351fb11d366b5c3fe9d2c8a7a76 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 29 Feb 2024 17:25:02 +0600 Subject: [PATCH 18/23] fix tests --- DuckDuckGo.xcodeproj/project.pbxproj | 12 +-- IntegrationTests/Tab/TabContentTests.swift | 112 +++++++++++++-------- IntegrationTests/Tab/empty.pdf | Bin 3833 -> 0 bytes IntegrationTests/Tab/test.pdf | Bin 0 -> 10855 bytes 4 files changed, 74 insertions(+), 50 deletions(-) delete mode 100644 IntegrationTests/Tab/empty.pdf create mode 100644 IntegrationTests/Tab/test.pdf diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a3ec5b0221..e2f1a5b73e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2728,6 +2728,8 @@ B64C85422694590B0048FEBE /* PermissionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C85412694590B0048FEBE /* PermissionButton.swift */; }; B64CE01E2B8622D700126CA5 /* AddressBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */; }; B64CE01F2B8622D700126CA5 /* AddressBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */; }; + B64E42AB2B909DC9006C1346 /* test.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B64E42AA2B909DC9006C1346 /* test.pdf */; }; + B64E42AC2B909DC9006C1346 /* test.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B64E42AA2B909DC9006C1346 /* test.pdf */; }; B65211252B29A42C00B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; }; B65211262B29A42E00B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; }; B65211272B29A43000B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; }; @@ -3035,8 +3037,6 @@ B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = B6EC37FE29B8D915001ACE79 /* Configuration */; }; B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; - B6EEDD7F2B8C6B8B00637EBC /* empty.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B6EEDD792B8C65F000637EBC /* empty.pdf */; }; - B6EEDD802B8C6B8C00637EBC /* empty.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B6EEDD792B8C65F000637EBC /* empty.pdf */; }; B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F41030264D2B23003DA42C /* ProgressExtension.swift */; }; B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; }; @@ -4232,6 +4232,7 @@ B64C853C26944B940048FEBE /* PermissionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionStore.swift; sourceTree = ""; }; B64C85412694590B0048FEBE /* PermissionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionButton.swift; sourceTree = ""; }; B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarTests.swift; sourceTree = ""; }; + B64E42AA2B909DC9006C1346 /* test.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test.pdf; sourceTree = ""; }; B65349A9265CF45000DCC645 /* DispatchQueueExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueExtensionsTests.swift; sourceTree = ""; }; B6553691268440D700085A79 /* WKProcessPool+GeolocationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKProcessPool+GeolocationProvider.swift"; sourceTree = ""; }; B65536962684413900085A79 /* WKGeolocationProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKGeolocationProvider.h; sourceTree = ""; }; @@ -4427,7 +4428,6 @@ B6EC37EA29B5DA2A001ACE79 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; B6EC37FA29B6447F001ACE79 /* TestsServer.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TestsServer.xcconfig; sourceTree = ""; }; B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = ""; }; - B6EEDD792B8C65F000637EBC /* empty.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = empty.pdf; sourceTree = ""; }; B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabContentTests.swift; sourceTree = ""; }; B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = ""; }; B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; @@ -7902,7 +7902,7 @@ B644B43C29D56811003FA9AB /* Tab */ = { isa = PBXGroup; children = ( - B6EEDD792B8C65F000637EBC /* empty.pdf */, + B64E42AA2B909DC9006C1346 /* test.pdf */, B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */, B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */, B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */, @@ -9054,7 +9054,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B6EEDD802B8C6B8C00637EBC /* empty.pdf in Resources */, + B64E42AC2B909DC9006C1346 /* test.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9069,7 +9069,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B6EEDD7F2B8C6B8B00637EBC /* empty.pdf in Resources */, + B64E42AB2B909DC9006C1346 /* test.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index 1d3508fa57..6c4d2ccf22 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppKit import Combine import Common import XCTest @@ -50,7 +51,7 @@ class TabContentTests: XCTestCase { @MainActor func testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws { - let pdfUrl = Bundle(for: Self.self).url(forResource: "empty", withExtension: "pdf")! + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! // open Tab with PDF let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -103,6 +104,10 @@ class TabContentTests: XCTestCase { try await Task.sleep(interval: 0.01) } } + let printOperationPromise = tab.$userInteractionDialog.compactMap { (dialog: Tab.UserDialog?) -> NSPrintOperation? in + guard case .print(let request) = dialog?.dialog else { return nil } + return request.parameters + }.timeout(5).first().promise() XCTAssertNotNil(printMenuItem.action) XCTAssertNotNil(printMenuItem.pdfHudRepresentedObject) @@ -118,13 +123,15 @@ class TabContentTests: XCTestCase { defer { window.endSheet(printDialog, returnCode: .cancel) } + let printOperation = try await printOperationPromise.value XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "…")) + XCTAssertEqual(printOperation.pageRange, NSRange(location: 1, length: 3)) } @MainActor - func disabled_testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws { - let pdfUrl = Bundle(for: Self.self).url(forResource: "empty", withExtension: "pdf")! + func testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! // open Tab with PDF let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -146,51 +153,69 @@ class TabContentTests: XCTestCase { pressure: 1)! // wait for context menu to appear - let menuWindowPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in - print(NSApp.windows.map(\.className)) - return NSApp.windows.first(where: { - $0.className == "NSPopupMenuWindow" - }) - }.timeout(5).first().promise() + let eMenuShown = expectation(description: "menu shown") + var menuItems = [NSMenuItem]() + NSView.swizzleWillOpenMenu { menu, event in + menuItems = menu.items + menu.removeAllItems() + eMenuShown.fulfill() + } // right-click + NSApp.activate(ignoringOtherApps: true) window.sendEvent(mouseDown) - let menuWindow = try await menuWindowPromise.value + await fulfillment(of: [eMenuShown]) // find Print, Save As -// let menuItems = menuWindow.contentView?.recursivelyFindMenuItemViews() -// let printMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.printMenuItem }) -// let saveAsMenuItem = menuItems?.first(where: { $0.menuItem.title == UserText.mainMenuFileSaveAs }) -// XCTAssertNotNil(printMenuItem) -// XCTAssertNotNil(saveAsMenuItem) -// -// // wait for save dialog to appear -// let saveDialogPromise = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect().compactMap { _ in -// self.window.sheets.first as? NSSavePanel -// }.timeout(5).first().promise() -// -// // Click Save As… -// saveAsMenuItem?.menuItem.accessibilityPerformPress() -// -// let saveDialog = try await saveDialogPromise.value -// guard let url = saveDialog.url else { -// XCTFail("no Save Dialog url") -// return -// } -// try? FileManager.default.removeItem(at: url) -// -// // wait until file is saved -// let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in -// FileManager.default.fileExists(atPath: url.path) -// }.timeout(5).first().promise() -// defer { -// try? FileManager.default.removeItem(at: url) -// } -// -// window.endSheet(saveDialog, returnCode: .OK) -// -// _=try await fileSavedPromise.value -// try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) + let printMenuItem = menuItems.first(where: { $0.title == UserText.printMenuItem }) + XCTAssertNotNil(printMenuItem) + guard let saveAsMenuItem = menuItems.first(where: { $0.title == UserText.mainMenuFileSaveAs }) else { + XCTFail("No Save As menu item") + return + } + + // wait for print dialog to appear + let eSaveDialogShown = expectation(description: "Save dialog shown") + let getSaveDialog = Task { @MainActor in + while true { + if let sheet = self.window.sheets.first as? NSSavePanel { + eSaveDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } + + XCTAssertNotNil(saveAsMenuItem.action) + XCTAssertNotNil(saveAsMenuItem.pdfHudRepresentedObject) + + // Click Save As… + _=saveAsMenuItem.action.map { action in + NSApp.sendAction(action, to: saveAsMenuItem.target, from: saveAsMenuItem) + } + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eSaveDialogShown], timeout: 5) { + getSaveDialog.cancel() + } + let saveDialog = try await getSaveDialog.value + + guard let url = saveDialog.url else { + XCTFail("no Save Dialog url") + return + } + try? FileManager.default.removeItem(at: url) + + // wait until file is saved + let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in + FileManager.default.fileExists(atPath: url.path) + }.timeout(5).first().promise() + defer { + try? FileManager.default.removeItem(at: url) + } + + window.endSheet(saveDialog, returnCode: .OK) + + _=try await fileSavedPromise.value + try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) } } @@ -214,7 +239,6 @@ private extension NSView { self.willOpenMenuWithEvent = willOpenMenuWithEvent } - @objc dynamic func swizzled_willOpenMenu(_ menu: NSMenu, with event: NSEvent) { if let willOpenMenuWithEvent = Self.willOpenMenuWithEvent { willOpenMenuWithEvent(menu, event) diff --git a/IntegrationTests/Tab/empty.pdf b/IntegrationTests/Tab/empty.pdf deleted file mode 100644 index d5b4c5c817d9440d1e45ae42d42a997b303bf292..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3833 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 BakBsb diff --git a/IntegrationTests/Tab/test.pdf b/IntegrationTests/Tab/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fd772c0fc07b00ee2c977018a9fa236bfa52f6a8 GIT binary patch literal 10855 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 Date: Thu, 29 Feb 2024 18:08:47 +0600 Subject: [PATCH 19/23] final touches --- DuckDuckGo/MainWindow/MainView.swift | 26 +++++++---- IntegrationTests/Tab/TabContentTests.swift | 54 +++++++++++----------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/DuckDuckGo/MainWindow/MainView.swift b/DuckDuckGo/MainWindow/MainView.swift index f8e09d70ea..5dc064ac99 100644 --- a/DuckDuckGo/MainWindow/MainView.swift +++ b/DuckDuckGo/MainWindow/MainView.swift @@ -18,6 +18,7 @@ import Cocoa import Combine +import WebKit final class MainView: NSView { let tabBarContainerView = NSView() @@ -133,15 +134,24 @@ final class MainView: NSView { } private func setupSaveAsAndPrintMenuItems(menu: NSMenu, with event: NSEvent) { - let hudView: WKPDFHUDViewWrapper? = withMouseLocationInViewCoordinates(event.locationInWindow) { point in - guard let view = self.hitTest(point) else { return nil } - if let hudView = WKPDFHUDViewWrapper(view: view) { - return hudView - } else if let webView = view as? WKWebView { - return webView.hudView(at: webView.convert(point, from: self)) + guard let window else { return } + + // try to find PDF HUD view at the right-click location (it might be a frame click) + let hudView: WKPDFHUDViewWrapper? = { + for point in [event.locationInWindow, window.mouseLocationOutsideOfEventStream] { + let locationInView = convert(point, from: nil) + guard let view = self.hitTest(locationInView) else { continue } + + if let hudView = WKPDFHUDViewWrapper(view: view) { + return hudView + } else if let webView = view as? WKWebView, + let hudView = webView.hudView(at: webView.convert(locationInView, from: self)) { + return hudView + } } - return nil - } + return (self.hitTest(bounds.center) as? WKWebView)?.hudView() + }() + assert(hudView != nil) // insert Save As… and Print… items after `Open with Preview` // 1. find `Copy` diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index 6c4d2ccf22..e9ab2541f8 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -47,6 +47,31 @@ class TabContentTests: XCTestCase { NSView.swizzleWillOpenMenu(with: nil) } + func sendRightMouseClick(to view: NSView) { + let point = view.convert(view.bounds.center, to: nil) + + let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown, + location: point, + modifierFlags: [], + timestamp: CACurrentMediaTime(), + windowNumber: window.windowNumber, + context: nil, + eventNumber: -22966, + clickCount: 1, + pressure: 1)! + let mouseUp = NSEvent.mouseEvent(with: .rightMouseUp, + location: point, + modifierFlags: [], + timestamp: CACurrentMediaTime(), + windowNumber: window.windowNumber, + context: nil, + eventNumber: -22966, + clickCount: 1, + pressure: 1)! + view.window!.sendEvent(mouseDown) + view.window!.sendEvent(mouseUp) + } + // MARK: - Tests @MainActor @@ -59,18 +84,6 @@ class TabContentTests: XCTestCase { let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value - let point = tab.webView.convert(tab.webView.bounds.center, to: nil) - - let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown, - location: point, - modifierFlags: [], - timestamp: CACurrentMediaTime(), - windowNumber: window.windowNumber, - context: nil, - eventNumber: -22966, - clickCount: 1, - pressure: 1)! - // wait for context menu to appear let eMenuShown = expectation(description: "menu shown") var menuItems = [NSMenuItem]() @@ -82,7 +95,7 @@ class TabContentTests: XCTestCase { // right-click NSApp.activate(ignoringOtherApps: true) - window.sendEvent(mouseDown) + sendRightMouseClick(to: tab.webView) await fulfillment(of: [eMenuShown]) // find Print, Save As @@ -139,19 +152,6 @@ class TabContentTests: XCTestCase { let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value - let point = tab.webView.convert(tab.webView.bounds.center, to: nil) - - NSApp.activate(ignoringOtherApps: true) - let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown, - location: point, - modifierFlags: [], - timestamp: CACurrentMediaTime(), - windowNumber: window.windowNumber, - context: nil, - eventNumber: -22966, - clickCount: 1, - pressure: 1)! - // wait for context menu to appear let eMenuShown = expectation(description: "menu shown") var menuItems = [NSMenuItem]() @@ -163,7 +163,7 @@ class TabContentTests: XCTestCase { // right-click NSApp.activate(ignoringOtherApps: true) - window.sendEvent(mouseDown) + sendRightMouseClick(to: tab.webView) await fulfillment(of: [eMenuShown]) // find Print, Save As From e01bc85c949994a9057458e156c0ee6636c366cb Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 29 Feb 2024 18:14:38 +0600 Subject: [PATCH 20/23] add main menu print/save tests --- IntegrationTests/Tab/TabContentTests.swift | 99 ++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index e9ab2541f8..13ba1fc393 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -17,6 +17,7 @@ // import AppKit +import Carbon import Combine import Common import XCTest @@ -142,6 +143,51 @@ class TabContentTests: XCTestCase { XCTAssertEqual(printOperation.pageRange, NSRange(location: 1, length: 3)) } + @MainActor + func testWhenPDFMainMenuPrintChosen_printDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // wait for print dialog to appear + let ePrintDialogShown = expectation(description: "Print dialog shown") + let getPrintDialog = Task { @MainActor in + while true { + if let sheet = self.window.sheets.first { + ePrintDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } + let printOperationPromise = tab.$userInteractionDialog.compactMap { (dialog: Tab.UserDialog?) -> NSPrintOperation? in + guard case .print(let request) = dialog?.dialog else { return nil } + return request.parameters + }.timeout(5).first().promise() + + // Hit Cmd+P + let keyDown = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "p", charactersIgnoringModifiers: "p", isARepeat: false, keyCode: UInt16(kVK_ANSI_P))! + let keyUp = NSEvent.keyEvent(with: .keyUp, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "p", charactersIgnoringModifiers: "p", isARepeat: false, keyCode: UInt16(kVK_ANSI_P))! + window.sendEvent(keyDown) + window.sendEvent(keyUp) + + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) { + getPrintDialog.cancel() + } + let printDialog = try await getPrintDialog.value + defer { + window.endSheet(printDialog, returnCode: .cancel) + } + let printOperation = try await printOperationPromise.value + + XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "…")) + XCTAssertEqual(printOperation.pageRange, NSRange(location: 1, length: 3)) + } + @MainActor func testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws { let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! @@ -218,6 +264,59 @@ class TabContentTests: XCTestCase { try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) } + @MainActor + func testWhenPDFMainMenuSaveAsChosen_saveDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // wait for print dialog to appear + let eSaveDialogShown = expectation(description: "Save dialog shown") + let getSaveDialog = Task { @MainActor in + while true { + if let sheet = self.window.sheets.first as? NSSavePanel { + eSaveDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } + + // Hit Cmd+S + let keyDown = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "s", charactersIgnoringModifiers: "s", isARepeat: false, keyCode: UInt16(kVK_ANSI_S))! + let keyUp = NSEvent.keyEvent(with: .keyUp, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "s", charactersIgnoringModifiers: "s", isARepeat: false, keyCode: UInt16(kVK_ANSI_S))! + window.sendEvent(keyDown) + window.sendEvent(keyUp) + + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eSaveDialogShown], timeout: 5) { + getSaveDialog.cancel() + } + let saveDialog = try await getSaveDialog.value + + guard let url = saveDialog.url else { + XCTFail("no Save Dialog url") + return + } + try? FileManager.default.removeItem(at: url) + + // wait until file is saved + let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in + FileManager.default.fileExists(atPath: url.path) + }.timeout(5).first().promise() + defer { + try? FileManager.default.removeItem(at: url) + } + + window.endSheet(saveDialog, returnCode: .OK) + + _=try await fileSavedPromise.value + try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) + } + } private extension NSView { From 37161e5c99ea467dae5499f34f80f55b936db846 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Mar 2024 10:31:24 +0600 Subject: [PATCH 21/23] extend test timeout --- IntegrationTests/Tab/ErrorPageTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift index 8e6bf5c61b..a017be5666 100644 --- a/IntegrationTests/Tab/ErrorPageTests.swift +++ b/IntegrationTests/Tab/ErrorPageTests.swift @@ -219,7 +219,7 @@ class ErrorPageTests: XCTestCase { tabsViewModel.select(at: .unpinned(0)) _=try await eNavigationSucceeded.value - await fulfillment(of: [eServerQueried], timeout: 1) + await fulfillment(of: [eServerQueried], timeout: 5) XCTAssertEqual(tab1.content.url, .test) XCTAssertNil(tab1.error) } From ffc5269a4d631aadfa93489b8a04f91e62a865e2 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Mar 2024 10:43:35 +0600 Subject: [PATCH 22/23] fixing testWhenTabWithNoConnectionErrorActivated_reloadTriggered --- IntegrationTests/Tab/ErrorPageTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift index a017be5666..d5cba03831 100644 --- a/IntegrationTests/Tab/ErrorPageTests.swift +++ b/IntegrationTests/Tab/ErrorPageTests.swift @@ -187,21 +187,22 @@ class ErrorPageTests: XCTestCase { // open 2 Tabs with newtab page let tab1 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + tabsViewModel.select(at: .unpinned(0)) window = WindowsManager.openNewWindow(with: tabsViewModel)! // wait until Home page loads - let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to a failing url + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() schemeHandler.middleware = [{ _ in .failure(NSError.noConnection) }] + tab1.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) // wait for error page to open - let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() - _=try await eNavigationFailed.value // switch to tab 2 @@ -209,13 +210,12 @@ class ErrorPageTests: XCTestCase { // next load should be ok let eServerQueried = expectation(description: "server request sent") + let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() schemeHandler.middleware = [{ _ in eServerQueried.fulfill() return .ok(.html(Self.testHtml)) }] // coming back to the failing tab 1 should trigger its reload - let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() - tabsViewModel.select(at: .unpinned(0)) _=try await eNavigationSucceeded.value From 1086c365b3170603da7ce42e70f52741cc3a2403 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Mar 2024 11:00:57 +0600 Subject: [PATCH 23/23] fixing Error page tests --- IntegrationTests/Tab/ErrorPageTests.swift | 44 +++++++++++++---------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift index d5cba03831..bb89e9c535 100644 --- a/IntegrationTests/Tab/ErrorPageTests.swift +++ b/IntegrationTests/Tab/ErrorPageTests.swift @@ -143,23 +143,25 @@ class ErrorPageTests: XCTestCase { NSError.disableSwizzledDescription = false } + // MARK: - Tests + func testWhenPageFailsToLoad_errorPageShown() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, fail with error schemeHandler.middleware = [{ _ in .failure(NSError.hostNotFound) }] - tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) let error = try await eNavigationFailed.value _=try await eNavigationFinished.value @@ -188,15 +190,18 @@ class ErrorPageTests: XCTestCase { let tab1 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let eNewtab2PageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) tabsViewModel.select(at: .unpinned(0)) window = WindowsManager.openNewWindow(with: tabsViewModel)! // wait until Home page loads try await eNewtabPageLoaded.value + try await eNewtab2PageLoaded.value // navigate to a failing url let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let eErrorPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() schemeHandler.middleware = [{ _ in .failure(NSError.noConnection) }] @@ -204,6 +209,7 @@ class ErrorPageTests: XCTestCase { tab1.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) // wait for error page to open _=try await eNavigationFailed.value + _=try await eErrorPageLoaded.value // switch to tab 2 tabsViewModel.select(at: .unpinned(1)) @@ -218,8 +224,8 @@ class ErrorPageTests: XCTestCase { // coming back to the failing tab 1 should trigger its reload tabsViewModel.select(at: .unpinned(0)) - _=try await eNavigationSucceeded.value await fulfillment(of: [eServerQueried], timeout: 5) + _=try await eNavigationSucceeded.value XCTAssertEqual(tab1.content.url, .test) XCTAssertNil(tab1.error) } @@ -232,13 +238,13 @@ class ErrorPageTests: XCTestCase { }] let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) window = WindowsManager.openNewWindow(with: tabsViewModel)! // wait for error page to open - let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() - let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() - _=try await eNavigationFailed.value _=try await eNavigationFinished.value @@ -285,12 +291,12 @@ class ErrorPageTests: XCTestCase { }] let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) window = WindowsManager.openNewWindow(with: tabsViewModel)! // wait for error page to open - let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() - let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() _=try await eNavigationFailed.value _=try await errorNavigationFinished.value @@ -314,11 +320,11 @@ class ErrorPageTests: XCTestCase { .failure(NSError.hostNotFound) }] let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() window = WindowsManager.openNewWindow(with: tab)! // wait for navigation to fail - let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() - let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() _=try await eNavigationFailed.value _=try await errorNavigationFinished.value @@ -366,11 +372,11 @@ class ErrorPageTests: XCTestCase { .failure(NSError.hostNotFound) }] let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() window = WindowsManager.openNewWindow(with: tab)! // wait for navigation to fail - let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() - let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() _=try await eNavigationFailed.value _=try await errorNavigationFinished.value @@ -420,9 +426,9 @@ class ErrorPageTests: XCTestCase { func testWhenPageLoadedAndFailsOnRefreshAndOnConsequentRefresh_errorPageIsUpdatedKeepingForwardHistory() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -496,9 +502,9 @@ class ErrorPageTests: XCTestCase { func testWhenPageLoadedAndFailsOnRefreshAndSucceedsOnConsequentRefresh_forwardHistoryIsPreserved() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -564,9 +570,9 @@ class ErrorPageTests: XCTestCase { func testWhenReloadingBySubmittingSameURL_errorPageRemainsSame() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -639,9 +645,9 @@ class ErrorPageTests: XCTestCase { func testWhenGoingToAnotherUrlFails_newBackForwardHistoryItemIsAdded() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -714,9 +720,9 @@ class ErrorPageTests: XCTestCase { func testWhenGoingToAnotherUrlSucceeds_newBackForwardHistoryItemIsAdded() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -786,9 +792,9 @@ class ErrorPageTests: XCTestCase { }] let tab = Tab(content: .url(.test, source: .pendingStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock, interactionStateData: Self.sessionStateData) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value XCTAssertTrue(tab.canReload) @@ -871,8 +877,8 @@ class ErrorPageTests: XCTestCase { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) - window = WindowsManager.openNewWindow(with: viewModel)! let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + window = WindowsManager.openNewWindow(with: viewModel)! try await eNewtabPageLoaded.value // navigate to alt url, redirect to test url, fail with error