Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
97c7254
Big ol backup
jonathanKingston Dec 20, 2024
8b76ca3
Backup of existing work
jonathanKingston Jan 24, 2025
39b2b03
Merge remote-tracking branch 'origin/main' into jkt/shared-web-tests
jonathanKingston Jan 24, 2025
7803231
Use launch args and logging
jonathanKingston Feb 4, 2025
7c02f28
Merge remote-tracking branch 'origin/main' into jkt/shared-web-tests
jonathanKingston Feb 4, 2025
f8ec2b4
Remove maestro specific changes
jonathanKingston Feb 4, 2025
6549f46
Add github workflow
jonathanKingston Feb 4, 2025
9051f24
Remove maestro specific setup
jonathanKingston Feb 4, 2025
8467795
Reorder action
jonathanKingston Feb 4, 2025
f3ab4cc
Move changes to extensions
jonathanKingston Feb 6, 2025
3c11508
Separate out automation steps into their own methods
jonathanKingston Feb 6, 2025
7043bf3
Merge remote-tracking branch 'origin/main' into jkt/shared-web-tests
jonathanKingston Feb 6, 2025
463ab99
Remove debug if statements
jonathanKingston Feb 6, 2025
d46f947
Remove force try in automation server
jonathanKingston Feb 6, 2025
8f82b7d
Rename startAutomationServer to startAutomationServerIfNeeded
jonathanKingston Feb 11, 2025
f05c8d7
Remove making controller public
jonathanKingston Feb 11, 2025
93ac4df
Make AutomationServer final
jonathanKingston Feb 11, 2025
7b00a95
Remove stray commented out global connection handler for Automation
jonathanKingston Feb 11, 2025
cbcd86f
Move currentURL method into direct access within the server
jonathanKingston Feb 11, 2025
e76cd77
Reduce nesting in AutomationServer
jonathanKingston Feb 11, 2025
4313f26
Return early by using guards for error states
jonathanKingston Feb 11, 2025
2fd77ec
Switch object identifiers to use tabModel.uid
jonathanKingston Feb 11, 2025
8f4a97f
Merge remote-tracking branch 'origin/main' into jkt/shared-web-tests
jonathanKingston Feb 11, 2025
784ddb5
Merge remote-tracking branch 'origin/main' into jkt/shared-web-tests
jonathanKingston Feb 12, 2025
6d71981
Automation fixes
jonathanKingston Feb 12, 2025
ef7df0c
Get mainViewController in automation server
jonathanKingston Feb 13, 2025
2b558e9
Simplify configuration of tests
jonathanKingston Feb 13, 2025
af83c26
Move testing steps to npm
jonathanKingston Feb 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use launch args and logging
  • Loading branch information
jonathanKingston committed Feb 4, 2025
commit 78032318107969076a77e7ddc91ff399324542c7
7 changes: 0 additions & 7 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ import Core

/// See: Launching.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//#if DEBUG
print("Automation port: \(launchOptionsHandler.automationPort ?? 0)")
if launchOptionsHandler.isUITesting {
AutomationServer(main: main, port: launchOptionsHandler.automationPort)
}
//#endif

let isTesting: Bool = ProcessInfo().arguments.contains("testing")
appStateMachine.handle(.didFinishLaunching(application, isTesting: isTesting))
return true
Expand Down
11 changes: 11 additions & 0 deletions DuckDuckGo/AppLifecycle/AppStates/Launching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import Combine
import PixelKit
import PixelExperimentKit


/// Represents the transient state where the app is being prepared for user interaction after being launched by the system.
/// - Usage:
/// - This state is typically associated with the `application(_:didFinishLaunchingWithOptions:)` method.
Expand Down Expand Up @@ -524,6 +525,7 @@ struct Launching: AppState {
}

tipKitAppEventsHandler.appDidFinishLaunching()
startAutomationServer()
}

private var appDependencies: AppDependencies {
Expand Down Expand Up @@ -634,6 +636,15 @@ struct Launching: AppState {
dataStoreIDManager: dataStoreIDManager)
}

private func startAutomationServer() {
Comment thread
jonathanKingston marked this conversation as resolved.
Outdated
//#if DEBUG
let launchOptionsHandler = LaunchOptionsHandler()
if launchOptionsHandler.isUITesting && launchOptionsHandler.automationPort != nil {
AutomationServer(main: mainViewController!, port: launchOptionsHandler.automationPort)
}
//#endif
}

}

extension Launching {
Expand Down
72 changes: 32 additions & 40 deletions DuckDuckGo/AutomationServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,15 @@
import Foundation
import Network

/*
// WebDriver BiDi automation server
class AutomationServerBidi {
let listener: NWListener
let main: MainViewController

init(main: MainViewController, port: Int?) {
self.main = main
listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(integerLiteral: UInt16(port ?? 8786)))
listener.newConnectionHandler = handleConnection
// listener.start(queue: .global())
listener.start(queue: .main)
}
extension Logger {
static var automationServer = { Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Automation Server") }()
}
*/


struct Log: TextOutputStream {

func write(_ string: String) {
let fm = FileManager.default
let log = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
let log = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log-automation.txt")
if let handle = try? FileHandle(forWritingTo: log) {
handle.seekToEndOfFile()
handle.write(string.data(using: .utf8)!)
Expand All @@ -54,16 +41,18 @@ struct Log: TextOutputStream {
class AutomationServer {
Comment thread
jonathanKingston marked this conversation as resolved.
Outdated
let listener: NWListener
let main: MainViewController
var logger: Log

init(main: MainViewController, port: Int?) {
self.logger = Log()

var port = port ?? 8786
self.main = main
listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(integerLiteral: UInt16(port ?? 8786)))
var log = Log()
print("Starting automation server on port \(port)", to: &log)
print("Bundle: \(Bundle.main.bundleIdentifier)", to: &log)
listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(integerLiteral: UInt16(port)))
listener.newConnectionHandler = handleConnection
// listener.start(queue: .global())
listener.start(queue: .main)
// Output server started
Logger.automationServer.info("Automation server started on port \(port)")
}

@MainActor
Expand All @@ -76,28 +65,28 @@ class AutomationServer {
case .ready:
break // Connection is valid, continue
case .cancelled, .failed:
print("Connection is no longer valid \(connection.state) \(error) \(content).", to: &self.logger)
print("Connection is no longer valid \(connection.state) \(String(describing: error)) \(String(describing: content)).")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace this print with logging (other places too)

return
default:
print("Connection is in state \(connection.state).", to: &self.logger)
print("Connection is in state \(connection.state).")
return
}
print("Received request! \(content) \(isComplete) \(error)", to: &self.logger)
Logger.automationServer.info("Received request! \(String(describing: content)) \(isComplete) \(String(describing: error))")

if let error {
print("Error: \(error)", to: &self.logger)
Logger.automationServer.error("Error: \(error)")
return
}

if let content {
print("Handling content", to: &self.logger)
Logger.automationServer.info("Handling content")
Task {
await self.processContentWhenReady(connection: connection, content: content)
}
}

if !isComplete {
print("Handling not complete", to: &self.logger)
Logger.automationServer.info("Handling not complete")
self.receive(from: connection)
}
}
Expand All @@ -112,17 +101,17 @@ class AutomationServer {
}

// Proceed when loading is complete
print("Handling content", to: &self.logger)
Logger.automationServer.info("Handling content")
self.handleConnection(connection, content)
}

@MainActor
func handleConnection(_ connection: NWConnection, _ content: Data) {
print("Handling request!", to: &self.logger)
Logger.automationServer.info("Handling request!")
let stringContent = String(decoding: content, as: UTF8.self)
// Log first line of string:
if let firstLine = stringContent.components(separatedBy: CharacterSet.newlines).first {
print(firstLine, to: &self.logger)
Logger.automationServer.info("First line: \(firstLine)")
}

func getQueryStringParameter(url: String, param: String) -> String? {
Expand All @@ -134,7 +123,7 @@ class AutomationServer {
if #available(iOS 16.0, *) {
Comment thread
jonathanKingston marked this conversation as resolved.
Outdated
let path = /^(GET|POST) (\/[^ ]*) HTTP/
if let match = stringContent.firstMatch(of: path) {
print("Path: \(match.2)", to: &logger)
Logger.automationServer.info("Path: \(match.2)")
// Convert the path into a URL object
guard let url = URL(string: String(match.2)) else {
print("Invalid URL: \(match.2)")
Expand All @@ -143,12 +132,11 @@ class AutomationServer {
if url.path == "/navigate" {
let navigateUrlString = getQueryStringParameter(url: String(match.2), param: "url") ?? ""
let navigateUrl = URL(string: navigateUrlString)!
self.main.navigateTo(url: navigateUrl)
self.main.loadUrl(navigateUrl)
self.respond(on: connection, response: "done")
} else if url.path == "/execute" {
let script = getQueryStringParameter(url: String(match.2), param: "script") ?? ""
var args: [String: String] = [:]
print("Script: \(script)", to: &self.logger)
// json decode args
if let argsString = getQueryStringParameter(url: String(match.2), param: "args") {
if let argsData = argsString.data(using: .utf8) {
Expand Down Expand Up @@ -192,15 +180,15 @@ class AutomationServer {
self.respond(on: connection, response: "{\"success\":true}")
} else if url.path == "/switchToWindow" {
if let handleString = getQueryStringParameter(url: String(match.2), param: "handle") {
print("Switch to window \(handleString)", to: &logger)
Logger.automationServer.info("Switch to window \(handleString)")
let tabToSelect: TabViewController? = nil
if let tabIndex = self.main.tabManager.model.tabs.firstIndex(where: { tab in
guard let tabView = self.main.tabManager.controller(for: tab) else {
return false
}
return String(UInt(bitPattern: ObjectIdentifier(tabView))) == handleString
}) {
print("found tab \(tabIndex)", to: &logger)
Logger.automationServer.info("found tab \(tabIndex)")
self.main.tabManager.select(tabAt: tabIndex)
self.respond(on: connection, response: "{\"success\":true}")
} else {
Expand Down Expand Up @@ -243,9 +231,9 @@ class AutomationServer {
}

func executeScript(_ script: String, args: [String: Any], on connection: NWConnection) async {
print("Going to execute script: \(script)", to: &self.logger)
Logger.automationServer.info("Going to execute script: \(script)")
var result = await main.executeScript(script, args: args)
print("Have result to execute script: \(result)", to: &self.logger)
Logger.automationServer.info("Have result to execute script: \(String(describing: result))")
guard var result else {
return
}
Expand All @@ -259,16 +247,20 @@ class AutomationServer {
// Try to encode the value to JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
Logger.automationServer.info("Have success value to execute script: \(String(describing: value))")

// Serialize the value to JSON if possible
if JSONSerialization.isValidJSONObject(value) {
if value == nil {
jsonString = "{}"
} else if JSONSerialization.isValidJSONObject(value) {
do {
let jsonData = try JSONSerialization.data(withJSONObject: value, options: [.prettyPrinted])
jsonString = String(data: jsonData, encoding: .utf8) ?? "{}"
} catch {
jsonString = "{\"error\": \"Failed to serialize value: \(error.localizedDescription)\"}"
}
} else {
Logger.automationServer.info("Have value that can't be encoded: \(String(describing: value))")
jsonString = "{\"error\": \"Value is not a valid JSON object\"}"
}

Expand Down Expand Up @@ -306,14 +298,14 @@ class AutomationServer {
content: response.data(using: .utf8),
completion: .contentProcessed({ error in
if let error = error {
print("Error sending response: \(error)", to: &self.logger)
Logger.automationServer.error("Error sending response: \(error)")
}
connection.cancel()
})
)
}
} catch {
print("Got error encoding JSON: \(error)", to: &logger)
Logger.automationServer.error("Got error encoding JSON: \(error)")
}
}

Expand Down
4 changes: 2 additions & 2 deletions DuckDuckGo/LaunchOptionsHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public final class LaunchOptionsHandler {
private static let appVariantName = "currentAppVariant"
private static let automationPort = "automationPort"

private let launchArguments: [String]
public let launchArguments: [String]
private let userDefaults: UserDefaults

public init(launchArguments: [String] = ProcessInfo.processInfo.arguments, userDefaults: UserDefaults = .app) {
Expand All @@ -34,7 +34,7 @@ public final class LaunchOptionsHandler {
}

public var isUITesting: Bool {
launchArguments.contains(Self.isUITesting)
launchArguments.contains(Self.isUITesting) || userDefaults.bool(forKey: Self.isUITesting)
}

public var isOnboardingCompleted: Bool {
Expand Down
3 changes: 1 addition & 2 deletions DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,7 @@ class MainViewController: UIViewController {
}

func startOnboardingFlowIfNotSeenBefore() {

guard ProcessInfo.processInfo.environment["ONBOARDING"] != "false" else {
Comment thread
jonathanKingston marked this conversation as resolved.
guard !LaunchOptionsHandler().isOnboardingCompleted else {
// explicitly skip onboarding, e.g. for integration tests
return
}
Expand Down