Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
path: build/SimStore.app.tar.bz2
archive: false
if-no-files-found: error
- name: Check Swift formatting
run: xcrun swift-format lint --recursive --strict src tests
- name: Test
run: ctest --preset release --verbose

Expand Down
6 changes: 3 additions & 3 deletions src/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import AppKit

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
true
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
true
}
}
38 changes: 19 additions & 19 deletions src/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@ import SwiftUI

@MainActor
struct ContentView: View {
@State private var cannotOpenRepository = false
@State private var cannotOpenRepository = false

private let repositoryURL = URL(string: "https://github.com/eagleoflqj/simstore-builds")!
private let repositoryURL = URL(string: "https://github.com/eagleoflqj/simstore-builds")!

var body: some View {
ContentUnavailableView {
Label("SimStore", systemImage: "square.stack.3d.up.fill")
} description: {
Text("Open-source apps for iOS Simulator.")
} actions: {
Button("View App Repository", systemImage: "arrow.up.right") {
cannotOpenRepository = !NSWorkspace.shared.open(repositoryURL)
}
.controlSize(.large)
}
.frame(minWidth: 640, minHeight: 420)
.alert("Couldn’t Open Repository", isPresented: $cannotOpenRepository) {
Button("OK", role: .cancel) {}
} message: {
Text("Open this address in your web browser: \(repositoryURL.absoluteString)")
}
var body: some View {
ContentUnavailableView {
Label("SimStore", systemImage: "square.stack.3d.up.fill")
} description: {
Text("Open-source apps for iOS Simulator.")
} actions: {
Button("View App Repository", systemImage: "arrow.up.right") {
cannotOpenRepository = !NSWorkspace.shared.open(repositoryURL)
}
.controlSize(.large)
}
.frame(minWidth: 640, minHeight: 420)
.alert("Couldn’t Open Repository", isPresented: $cannotOpenRepository) {
Button("OK", role: .cancel) {}
} message: {
Text("Open this address in your web browser: \(repositoryURL.absoluteString)")
}
}
}
12 changes: 6 additions & 6 deletions src/SimStoreApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import SwiftUI

@main
struct SimStoreApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate

var body: some Scene {
WindowGroup("SimStore") {
ContentView()
}
.defaultSize(width: 800, height: 540)
var body: some Scene {
WindowGroup("SimStore") {
ContentView()
}
.defaultSize(width: 800, height: 540)
}
}
50 changes: 25 additions & 25 deletions tests/AppLifecycleTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@ import AppKit

@main
struct AppLifecycleTest {
@MainActor
static func main() {
let application = NSApplication.shared
let delegate = AppDelegate()
application.delegate = delegate
application.setActivationPolicy(.regular)
@MainActor
static func main() {
let application = NSApplication.shared
let delegate = AppDelegate()
application.delegate = delegate
application.setActivationPolicy(.regular)

let window = NSWindow(
contentRect: NSRect(x: 200, y: 200, width: 640, height: 420),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
window.makeKeyAndOrderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
guard window.isVisible else {
fatalError("The test window did not appear.")
}
print("Closing the last window; the process must exit normally.")
window.performClose(nil)
}
withExtendedLifetime(delegate) {
application.run()
}
fatalError("The application event loop returned without terminating the process.")
let window = NSWindow(
contentRect: NSRect(x: 200, y: 200, width: 640, height: 420),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
window.makeKeyAndOrderFront(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
guard window.isVisible else {
fatalError("The test window did not appear.")
}
print("Closing the last window; the process must exit normally.")
window.performClose(nil)
}
withExtendedLifetime(delegate) {
application.run()
}
fatalError("The application event loop returned without terminating the process.")
}
}
120 changes: 64 additions & 56 deletions tests/AppSmokeTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,74 @@ import Foundation

@main
struct AppSmokeTest {
static func main() {
do {
try verifyWindow()
} catch {
FileHandle.standardError.write(Data("App smoke test failed: \(error.localizedDescription)\n".utf8))
exit(EXIT_FAILURE)
}
static func main() {
do {
try verifyWindow()
} catch {
FileHandle.standardError.write(
Data("App smoke test failed: \(error.localizedDescription)\n".utf8))
exit(EXIT_FAILURE)
}
}

static func verifyWindow() throws {
guard CommandLine.arguments.count == 2 else {
throw failure("Expected the App Bundle path.")
}
let bundleURL = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
guard let executable = Bundle(url: bundleURL)?.executableURL else {
throw failure("The App Bundle has no executable.")
}
let process = Process()
process.executableURL = executable
try process.run()
defer {
if process.isRunning {
process.terminate()
}
process.waitUntilExit()
}
static func verifyWindow() throws {
guard CommandLine.arguments.count == 2 else {
throw failure("Expected the App Bundle path.")
}
let bundleURL = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
guard let executable = Bundle(url: bundleURL)?.executableURL else {
throw failure("The App Bundle has no executable.")
}
let process = Process()
process.executableURL = executable
try process.run()
defer {
if process.isRunning {
process.terminate()
}
process.waitUntilExit()
}

let deadline = Date().addingTimeInterval(10)
var foundWindow = false
while Date() < deadline {
guard process.isRunning else {
throw failure("The app exited before its window appeared (status \(process.terminationStatus)).")
}
let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
foundWindow = windows.contains { window in
guard (window[kCGWindowOwnerPID as String] as? Int32) == process.processIdentifier,
(window[kCGWindowLayer as String] as? Int) == 0,
let bounds = window[kCGWindowBounds as String] as? [String: Any],
let width = bounds["Width"] as? Double,
let height = bounds["Height"] as? Double else {
return false
}
return width >= 600 && height >= 400
}
if foundWindow {
break
}
Thread.sleep(forTimeInterval: 0.1)
}
guard foundWindow else {
throw failure("No visible app window appeared. Run this test from a logged-in macOS GUI session.")
}
Thread.sleep(forTimeInterval: 2)
guard process.isRunning else {
throw failure("The app exited after showing its window.")
let deadline = Date().addingTimeInterval(10)
var foundWindow = false
while Date() < deadline {
guard process.isRunning else {
throw failure(
"The app exited before its window appeared (status \(process.terminationStatus)).")
}
let windows =
CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID)
as? [[String: Any]] ?? []
foundWindow = windows.contains { window in
guard (window[kCGWindowOwnerPID as String] as? Int32) == process.processIdentifier,
(window[kCGWindowLayer as String] as? Int) == 0,
let bounds = window[kCGWindowBounds as String] as? [String: Any],
let width = bounds["Width"] as? Double,
let height = bounds["Height"] as? Double
else {
return false
}
print("PASS: SimStore launched with a visible window and remained alive (PID \(process.processIdentifier)).")
return width >= 600 && height >= 400
}
if foundWindow {
break
}
Thread.sleep(forTimeInterval: 0.1)
}

static func failure(_ message: String) -> NSError {
NSError(domain: "SimStoreSmokeTest", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
guard foundWindow else {
throw failure(
"No visible app window appeared. Run this test from a logged-in macOS GUI session.")
}
Thread.sleep(forTimeInterval: 2)
guard process.isRunning else {
throw failure("The app exited after showing its window.")
}
print(
"PASS: SimStore launched with a visible window and remained alive (PID \(process.processIdentifier))."
)
}

static func failure(_ message: String) -> NSError {
NSError(domain: "SimStoreSmokeTest", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
}
}