Formatting

This commit is contained in:
cdricms
2024-07-09 20:35:00 +02:00
parent 6b3b26e4bc
commit dc99548d07
11 changed files with 545 additions and 508 deletions

View File

@@ -9,34 +9,37 @@ import SwiftUI
@main @main
struct BrewerApp: App { struct BrewerApp: App {
@AppStorage("isUpToDate") var isUpToDate: Bool = true @AppStorage("isUpToDate") var isUpToDate: Bool = true
init() { init() {
do { do {
let res = try shell("/opt/homebrew/bin/brew outdated --greedy-latest -g | wc -l") let res = try shell(
if let number = Int(res.trimmingCharacters(in: .whitespacesAndNewlines)), number > 0 { "/opt/homebrew/bin/brew outdated --greedy-latest -g | wc -l")
isUpToDate = false if let number = Int(
} res.trimmingCharacters(in: .whitespacesAndNewlines)), number > 0
} catch { {
print(error.localizedDescription) isUpToDate = false
} }
} } catch {
print(error.localizedDescription)
}
}
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
EmptyView() EmptyView()
} }
MenuBarExtra { MenuBarExtra {
ContentView().padding() ContentView().padding()
} label: { } label: {
if isUpToDate { if isUpToDate {
Label("Brewer", systemImage: "sparkle.magnifyingglass") Label("Brewer", systemImage: "sparkle.magnifyingglass")
} else { } else {
Label("Brewer", systemImage: "sparkle.magnifyingglass") Label("Brewer", systemImage: "sparkle.magnifyingglass")
.symbolRenderingMode(.palette) .symbolRenderingMode(.palette)
.foregroundStyle(.orange) .foregroundStyle(.orange)
} }
} }
.menuBarExtraStyle(.window) .menuBarExtraStyle(.window)
} }
} }

View File

@@ -8,31 +8,32 @@
import SwiftUI import SwiftUI
struct CaskDetailView: View { struct CaskDetailView: View {
var cask: Cask var cask: Cask
@Bindable var brewListing: Homebrew @Bindable var brewListing: Homebrew
var body: some View { var body: some View {
NavigationStack { NavigationStack {
VStack { VStack {
Text("Identifier: \(cask.fullToken)") Text("Identifier: \(cask.fullToken)")
Text("Version: \(cask.version)") Text("Version: \(cask.version)")
} }
.navigationTitle(cask.name.first ?? cask.fullToken) .navigationTitle(cask.name.first ?? cask.fullToken)
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
if cask.installed == nil { if cask.installed == nil {
DownloadButton(name: cask.fullToken, isCask: true) DownloadButton(name: cask.fullToken, isCask: true)
} else if cask.outdated { } else if cask.outdated {
Button("Update") { Button("Update") {
} }
} }
} }
if cask.installed != nil { if cask.installed != nil {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
UninstallButton(name: cask.fullToken, brewListing: brewListing) UninstallButton(
} name: cask.fullToken, brewListing: brewListing)
} }
} }
} }
} }
}
} }

View File

@@ -8,46 +8,46 @@
import SwiftUI import SwiftUI
struct DownloadButton: View { struct DownloadButton: View {
let name: String let name: String
var isCask: Bool = false var isCask: Bool = false
@State private var brew = Homebrew() @State private var brew = Homebrew()
@State private var downloaded = false @State private var downloaded = false
var body: some View { var body: some View {
VStack { VStack {
if downloaded { if downloaded {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.symbolRenderingMode(.palette) .symbolRenderingMode(.palette)
.symbolVariant(.circle) .symbolVariant(.circle)
.symbolVariant(.fill) .symbolVariant(.fill)
.foregroundStyle(.white, .green) .foregroundStyle(.white, .green)
} else if brew.isLoading { } else if brew.isLoading {
ProgressView() ProgressView()
.controlSize(.small) .controlSize(.small)
} else if brew.errorMessage != nil { } else if brew.errorMessage != nil {
Image(systemName: "x") Image(systemName: "x")
.symbolRenderingMode(.palette) .symbolRenderingMode(.palette)
.symbolVariant(.circle) .symbolVariant(.circle)
.symbolVariant(.fill) .symbolVariant(.fill)
.foregroundStyle(.white, .red) .foregroundStyle(.white, .red)
} else { } else {
Button("Get") { Button("Get") {
Task { Task {
downloaded = await brew.install(name, isCask: isCask) downloaded = await brew.install(name, isCask: isCask)
} }
} }
.buttonBorderShape(.capsule) .buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
} }
.onAppear { .onAppear {
Task { Task {
downloaded = await brew.isDownloaded(name) downloaded = await brew.isDownloaded(name)
} }
} }
} }
} }
#Preview { #Preview {
DownloadButton(name: "firefox") DownloadButton(name: "firefox")
} }

View File

@@ -8,38 +8,38 @@
import SwiftUI import SwiftUI
struct UninstallButton: View { struct UninstallButton: View {
let name: String let name: String
@State private var brew = Homebrew() @State private var brew = Homebrew()
@State private var uninstalled = false @State private var uninstalled = false
@Bindable var brewListing: Homebrew @Bindable var brewListing: Homebrew
var body: some View { var body: some View {
VStack { VStack {
if uninstalled { if uninstalled {
EmptyView() EmptyView()
} else if brew.isLoading { } else if brew.isLoading {
ProgressView() ProgressView()
.controlSize(.small) .controlSize(.small)
} else if brew.errorMessage != nil { } else if brew.errorMessage != nil {
Image(systemName: "x") Image(systemName: "x")
.symbolRenderingMode(.palette) .symbolRenderingMode(.palette)
.symbolVariant(.circle) .symbolVariant(.circle)
.symbolVariant(.fill) .symbolVariant(.fill)
.foregroundStyle(.white, .red) .foregroundStyle(.white, .red)
} else { } else {
Button("Uninstall", role: .destructive) { Button("Uninstall", role: .destructive) {
Task { Task {
uninstalled = await brew.uninstall(name) uninstalled = await brew.uninstall(name)
brewListing.getInstalled() brewListing.getInstalled()
} }
} }
.buttonBorderShape(.capsule) .buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
} }
.onAppear { .onAppear {
Task { Task {
uninstalled = await brew.isDownloaded(name) uninstalled = await brew.isDownloaded(name)
} }
} }
} }
} }

View File

@@ -8,29 +8,29 @@
import SwiftUI import SwiftUI
enum SegementedSelection: String, CaseIterable { enum SegementedSelection: String, CaseIterable {
case search = "Search" case search = "Search"
case installed = "Installed" case installed = "Installed"
} }
struct ContentView: View { struct ContentView: View {
@State private var segmentedSelection: SegementedSelection = .search @State private var segmentedSelection: SegementedSelection = .search
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Picker("", selection: $segmentedSelection) { Picker("", selection: $segmentedSelection) {
ForEach(SegementedSelection.allCases, id: \.self) { sel in ForEach(SegementedSelection.allCases, id: \.self) { sel in
Text(sel.rawValue).tag(sel) Text(sel.rawValue).tag(sel)
} }
}.pickerStyle(.segmented) }.pickerStyle(.segmented)
switch segmentedSelection { switch segmentedSelection {
case .search: case .search:
SearchView() SearchView()
case .installed: case .installed:
InstalledView() InstalledView()
} }
} }
} }
} }
#Preview { #Preview {
ContentView() ContentView()
} }

View File

@@ -8,101 +8,120 @@
import SwiftUI import SwiftUI
struct InstalledView: View { struct InstalledView: View {
@State private var brew = Homebrew() @State private var brew = Homebrew()
var body: some View { var body: some View {
VStack { VStack {
if brew.isLoading { if brew.isLoading {
ProgressView("Loading...") ProgressView("Loading...")
} else if let data = brew.data { } else if let data = brew.data {
List { List {
if !data.casks.isEmpty { if !data.casks.isEmpty {
Section { Section {
ForEach(data.casks, id: \.fullToken) { cask in ForEach(data.casks, id: \.fullToken) { cask in
NavigationLink { NavigationLink {
CaskDetailView(cask: cask, brewListing: brew) CaskDetailView(
} label: { cask: cask, brewListing: brew)
HStack { } label: {
Text(cask.name.first ?? cask.fullToken) HStack {
Spacer() Text(cask.name.first ?? cask.fullToken)
if cask.outdated { Spacer()
Button { if cask.outdated {
Button {
} label: { } label: {
Label("Update", systemImage: "arrow.counterclockwise.circle.fill") Label(
.symbolRenderingMode(.palette) "Update",
.foregroundStyle(.white, .orange) systemImage:
} "arrow.counterclockwise.circle.fill"
} else { )
Text(cask.version) .symbolRenderingMode(.palette)
} .foregroundStyle(
} .white, .orange)
.contextMenu { }
UninstallButton(name: cask.fullToken, brewListing: brew) } else {
} Text(cask.version)
} }
} }
} header: { .contextMenu {
HStack { UninstallButton(
Text("Casks") name: cask.fullToken,
Spacer() brewListing: brew)
Text("\(data.casks.count) installed") }
} }
} }
} } header: {
HStack {
Text("Casks")
Spacer()
Text("\(data.casks.count) installed")
}
}
}
if !data.formulae.isEmpty { if !data.formulae.isEmpty {
Section { Section {
ForEach(data.formulae, id: \.fullName) { formulae in ForEach(data.formulae, id: \.fullName) { formulae in
HStack { HStack {
Text(formulae.fullName) Text(formulae.fullName)
Spacer() Spacer()
if formulae.outdated { if formulae.outdated {
Button { Button {
} label: { } label: {
Label("Update", systemImage: "arrow.counterclockwise.circle.fill") Label(
.symbolRenderingMode(.palette) "Update",
.foregroundStyle(.white, .orange) systemImage:
} "arrow.counterclockwise.circle.fill"
} else { )
Text(formulae.versions.stable) .symbolRenderingMode(.palette)
} .foregroundStyle(.white, .orange)
} }
.contextMenu { } else {
UninstallButton(name: formulae.fullName, brewListing: brew) Text(formulae.versions.stable)
} }
} }
} header: { .contextMenu {
HStack { UninstallButton(
Text("Formulae") name: formulae.fullName,
Spacer() brewListing: brew)
Text("\(data.formulae.count) installed") }
} }
} } header: {
} HStack {
} Text("Formulae")
.listStyle(.inset(alternatesRowBackgrounds: true)) Spacer()
let areOutdated = data.casks.map(\.outdated).count + data.formulae.map(\.outdated).count Text("\(data.formulae.count) installed")
let _ = UserDefaults.standard.set(!(areOutdated > 0), forKey: "isUpToDate") }
if areOutdated > 0 { }
Button { }
}
.listStyle(.inset(alternatesRowBackgrounds: true))
let areOutdated =
data.casks.map(\.outdated).count
+ data.formulae.map(\.outdated).count
let _ = UserDefaults.standard.set(
!(areOutdated > 0), forKey: "isUpToDate")
if areOutdated > 0 {
Button {
} label: { } label: {
Label("Update all", systemImage: "arrow.counterclockwise.circle.fill") Label(
} "Update all",
.buttonStyle(.borderedProminent) systemImage: "arrow.counterclockwise.circle.fill")
.tint(.orange) }
} .buttonStyle(.borderedProminent)
} else if let error = brew.errorMessage { .tint(.orange)
Text(error) }
.foregroundStyle(.red) } else if let error = brew.errorMessage {
} Text(error)
}.task { .foregroundStyle(.red)
brew.getInstalled() }
} }.task {
} brew.getInstalled()
}
}
} }
#Preview { #Preview {
InstalledView() InstalledView()
} }

View File

@@ -8,207 +8,217 @@
import Foundation import Foundation
enum ShellError: Error { enum ShellError: Error {
case emptyOutput case emptyOutput
} }
@discardableResult @discardableResult
func shell(_ command: String) throws -> String { func shell(_ command: String) throws -> String {
let task = Process() let task = Process()
task.executableURL = URL(filePath: "/bin/bash") task.executableURL = URL(filePath: "/bin/bash")
task.arguments = ["-c", command] task.arguments = ["-c", command]
let pipe = Pipe() let pipe = Pipe()
task.standardError = pipe task.standardError = pipe
task.standardOutput = pipe task.standardOutput = pipe
try task.run() try task.run()
if let data = try pipe.fileHandleForReading.readToEnd(), if let data = try pipe.fileHandleForReading.readToEnd(),
let output = String(data: data, encoding: .utf8) let output = String(data: data, encoding: .utf8)
{ {
return output return output
} }
throw ShellError.emptyOutput throw ShellError.emptyOutput
} }
struct Cask: Codable { struct Cask: Codable {
let token: String let token: String
let fullToken: String let fullToken: String
let tap: String let tap: String
let name: [String] let name: [String]
let desc: String let desc: String
let homepage: String let homepage: String
let url: String let url: String
let version: String let version: String
let installed: String? let installed: String?
let outdated: Bool let outdated: Bool
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case token, tap, name, desc, homepage, url, version, installed, outdated case token, tap, name, desc, homepage, url, version, installed, outdated
case fullToken = "full_token" case fullToken = "full_token"
} }
} }
struct Formulae: Codable { struct Formulae: Codable {
struct Version: Codable { struct Version: Codable {
let stable: String let stable: String
let head: String? let head: String?
let bottle: Bool let bottle: Bool
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case stable case stable
case head, bottle case head, bottle
} }
} }
let name: String let name: String
let fullName: String let fullName: String
let tap: String let tap: String
let oldNames: [String] let oldNames: [String]
let aliases: [String] let aliases: [String]
let versionedFormulae: [String] let versionedFormulae: [String]
let desc: String let desc: String
let license: String? let license: String?
let homepage: String let homepage: String
let versions: Version let versions: Version
let outdated: Bool let outdated: Bool
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case name case name
case tap case tap
case aliases case aliases
case desc case desc
case license case license
case homepage case homepage
case versions case versions
case outdated case outdated
case fullName = "full_name" case fullName = "full_name"
case oldNames = "oldnames" case oldNames = "oldnames"
case versionedFormulae = "versioned_formulae" case versionedFormulae = "versioned_formulae"
} }
} }
struct InfoResponse: Codable { struct InfoResponse: Codable {
let formulae: [Formulae] let formulae: [Formulae]
let casks: [Cask] let casks: [Cask]
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case casks case casks
case formulae case formulae
} }
} }
@Observable @Observable
class Homebrew { class Homebrew {
var data: InfoResponse? var data: InfoResponse?
var isLoading = false var isLoading = false
var errorMessage: String? var errorMessage: String?
func getInfo(on query: String) { func getInfo(on query: String) {
self.isLoading = true self.isLoading = true
self.errorMessage = nil self.errorMessage = nil
self.data = nil self.data = nil
Task { [weak self] in Task { [weak self] in
do { do {
let res = try shell("/opt/homebrew/bin/brew info --json=v2 \(query)") let res = try shell(
if let data = res.data(using: .utf8) { "/opt/homebrew/bin/brew info --json=v2 \(query)")
let output = try JSONDecoder().decode(InfoResponse.self, from: data) if let data = res.data(using: .utf8) {
self?.data = output let output = try JSONDecoder().decode(
} InfoResponse.self, from: data)
} catch { self?.data = output
self?.errorMessage = error.localizedDescription }
} } catch {
self?.isLoading = false self?.errorMessage = error.localizedDescription
} }
} self?.isLoading = false
}
}
func getInstalled() { func getInstalled() {
self.isLoading = true self.isLoading = true
self.data = nil self.data = nil
self.errorMessage = nil self.errorMessage = nil
Task { [weak self] in Task { [weak self] in
do { do {
let res = try shell("/opt/homebrew/bin/brew info --json=v2 --installed") let res = try shell(
if let data = res.data(using: .utf8) { "/opt/homebrew/bin/brew info --json=v2 --installed")
let output = try JSONDecoder().decode(InfoResponse.self, from: data) if let data = res.data(using: .utf8) {
self?.data = output let output = try JSONDecoder().decode(
} InfoResponse.self, from: data)
} catch { self?.data = output
self?.errorMessage = error.localizedDescription }
} } catch {
self?.isLoading = false self?.errorMessage = error.localizedDescription
} }
self?.isLoading = false
}
} }
func isDownloaded(_ name: String) async -> Bool { func isDownloaded(_ name: String) async -> Bool {
self.isLoading = true self.isLoading = true
self.errorMessage = nil self.errorMessage = nil
self.data = nil self.data = nil
let task = Task { [weak self] in let task = Task { [weak self] in
do { do {
let res = try shell("/opt/homebrew/bin/brew list -1 \(name) >/dev/null 2>&1; echo $?") let res = try shell(
.trimmingCharacters(in: .whitespacesAndNewlines) "/opt/homebrew/bin/brew list -1 \(name) >/dev/null 2>&1; echo $?"
self?.isLoading = false )
return res == "0" .trimmingCharacters(in: .whitespacesAndNewlines)
} catch { self?.isLoading = false
self?.errorMessage = error.localizedDescription return res == "0"
} } catch {
self?.errorMessage = error.localizedDescription
}
return false return false
} }
switch await task.result { switch await task.result {
case .success(let success): case .success(let success):
return success return success
case .failure(let fail): case .failure(let fail):
return false return false
} }
} }
func install(_ fullToken: String, isCask: Bool = false) async -> Bool { func install(_ fullToken: String, isCask: Bool = false) async -> Bool {
self.isLoading = true self.isLoading = true
self.data = nil self.data = nil
let task = Task { [weak self] in let task = Task { [weak self] in
do { do {
try shell("/opt/homebrew/bin/brew install \(isCask ? "--cask" : "") \(fullToken)") try shell(
} catch { "/opt/homebrew/bin/brew install \(isCask ? "--cask" : "") \(fullToken)"
self?.errorMessage = error.localizedDescription )
} } catch {
self?.isLoading = false self?.errorMessage = error.localizedDescription
return await self?.isDownloaded(fullToken) ?? false }
} self?.isLoading = false
return await self?.isDownloaded(fullToken) ?? false
}
return switch await task.result { return switch await task.result {
case .success(let success): case .success(let success):
success success
case .failure(let fail): case .failure(let fail):
false false
} }
} }
func uninstall(_ fullToken: String) async -> Bool { func uninstall(_ fullToken: String) async -> Bool {
self.isLoading = true self.isLoading = true
self.data = nil self.data = nil
let task = Task { [weak self] in let task = Task { [weak self] in
do { do {
let res = let res =
try shell("/opt/homebrew/bin/brew uninstall \(fullToken); echo $?") try shell(
.trimmingCharacters(in: .whitespacesAndNewlines) "/opt/homebrew/bin/brew uninstall \(fullToken); echo $?"
} catch { )
self?.errorMessage = error.localizedDescription .trimmingCharacters(in: .whitespacesAndNewlines)
} } catch {
self?.isLoading = false self?.errorMessage = error.localizedDescription
return await !(self?.isDownloaded(fullToken) ?? true) }
} self?.isLoading = false
return await !(self?.isDownloaded(fullToken) ?? true)
}
return switch await task.result { return switch await task.result {
case .success(let success): case .success(let success):
success success
case .failure(let failure): case .failure(let failure):
false false
} }
} }
} }

View File

@@ -8,58 +8,61 @@
import SwiftUI import SwiftUI
struct SearchView: View { struct SearchView: View {
@State private var query = "" @State private var query = ""
@State private var brew = Homebrew() @State private var brew = Homebrew()
var body: some View { var body: some View {
VStack { VStack {
TextField("Search", text: $query) TextField("Search", text: $query)
.padding() .padding()
.padding(.bottom, 0) .padding(.bottom, 0)
.onSubmit { .onSubmit {
brew.getInfo(on: query) brew.getInfo(on: query)
} }
Spacer() Spacer()
if brew.isLoading { if brew.isLoading {
ProgressView("Loading...") ProgressView("Loading...")
} else if let data = brew.data { } else if let data = brew.data {
List { List {
if !data.casks.isEmpty { if !data.casks.isEmpty {
Section("Casks") { Section("Casks") {
ForEach(data.casks, id: \.fullToken) { cask in ForEach(data.casks, id: \.fullToken) { cask in
HStack { HStack {
Text(cask.fullToken) Text(cask.fullToken)
Spacer() Spacer()
if cask.installed != nil { if cask.installed != nil {
Image(systemName: "checkmark.circle.fill") Image(
.symbolRenderingMode(.palette) systemName: "checkmark.circle.fill"
.foregroundStyle(.white, .green) )
} else { .symbolRenderingMode(.palette)
DownloadButton(name: cask.fullToken, isCask: true) .foregroundStyle(.white, .green)
} } else {
} DownloadButton(
} name: cask.fullToken, isCask: true)
} }
} }
if !data.formulae.isEmpty { }
}
}
if !data.formulae.isEmpty {
Section("Formulaes") { Section("Formulaes") {
ForEach(data.formulae, id: \.fullName) { formulae in ForEach(data.formulae, id: \.fullName) { formulae in
HStack { HStack {
Text(formulae.fullName) Text(formulae.fullName)
} }
} }
} }
} }
} }
} else if let errorMessage = brew.errorMessage { } else if let errorMessage = brew.errorMessage {
Text(errorMessage) Text(errorMessage)
.foregroundStyle(.red) .foregroundStyle(.red)
} }
} }
} }
} }
#Preview { #Preview {
SearchView() SearchView()
} }

View File

@@ -6,31 +6,32 @@
// //
import XCTest import XCTest
@testable import Brewer @testable import Brewer
final class BrewerTests: XCTestCase { final class BrewerTests: XCTestCase {
override func setUpWithError() throws { override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class. // Put setup code here. This method is called before the invocation of each test method in the class.
} }
override func tearDownWithError() throws { override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
func testExample() throws { func testExample() throws {
// This is an example of a functional test case. // This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results. // Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async. // Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
} }
func testPerformanceExample() throws { func testPerformanceExample() throws {
// This is an example of a performance test case. // This is an example of a performance test case.
self.measure { self.measure {
// Put the code you want to measure the time of here. // Put the code you want to measure the time of here.
} }
} }
} }

View File

@@ -9,33 +9,33 @@ import XCTest
final class BrewerUITests: XCTestCase { final class BrewerUITests: XCTestCase {
override func setUpWithError() throws { override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class. // Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs. // In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. // In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
} }
override func tearDownWithError() throws { override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
func testExample() throws { func testExample() throws {
// UI tests must launch the application that they test. // UI tests must launch the application that they test.
let app = XCUIApplication() let app = XCUIApplication()
app.launch() app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results. // Use XCTAssert and related functions to verify your tests produce the correct results.
} }
func testLaunchPerformance() throws { func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application. // This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) { measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch() XCUIApplication().launch()
} }
} }
} }
} }

View File

@@ -9,24 +9,24 @@ import XCTest
final class BrewerUITestsLaunchTests: XCTestCase { final class BrewerUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool { override class var runsForEachTargetApplicationUIConfiguration: Bool {
true true
} }
override func setUpWithError() throws { override func setUpWithError() throws {
continueAfterFailure = false continueAfterFailure = false
} }
func testLaunch() throws { func testLaunch() throws {
let app = XCUIApplication() let app = XCUIApplication()
app.launch() app.launch()
// Insert steps here to perform after app launch but before taking a screenshot, // Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app // such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot()) let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen" attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways attachment.lifetime = .keepAlways
add(attachment) add(attachment)
} }
} }