Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
563f0e5d4a | ||
|
|
cd4665e7a9 | ||
|
|
f78da50a59 |
@@ -15,14 +15,26 @@ public enum ArtefactFiles {
|
|||||||
try Data(contentsOf: url).base64EncodedString()
|
try Data(contentsOf: url).base64EncodedString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `get_app_info` — version + build for the About dialog.
|
/// `get_app_info` — version, build, and build date for the About dialog.
|
||||||
public static func appInfo(
|
public static func appInfo(
|
||||||
bundle: Bundle = .main
|
bundle: Bundle = .main
|
||||||
) -> (version: String, build: String) {
|
) -> (version: String, build: String, buildDate: String) {
|
||||||
let info = bundle.infoDictionary ?? [:]
|
let info = bundle.infoDictionary ?? [:]
|
||||||
return (
|
let version = info["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
||||||
info["CFBundleShortVersionString"] as? String ?? "0.0.0",
|
let build = info["CFBundleVersion"] as? String ?? "0"
|
||||||
info["CFBundleVersion"] as? String ?? "0"
|
|
||||||
)
|
let url = bundle.executableURL ?? bundle.bundleURL
|
||||||
|
let buildDate: String
|
||||||
|
if let values = try? url.resourceValues(forKeys: [.contentModificationDateKey]),
|
||||||
|
let date = values.contentModificationDate {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .none
|
||||||
|
buildDate = formatter.string(from: date)
|
||||||
|
} else {
|
||||||
|
buildDate = "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (version, build, buildDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ICCeryCore
|
||||||
|
|
||||||
|
/// About dialog for ICCery (issue #31, docs/21 §Modals).
|
||||||
|
struct AboutView: View {
|
||||||
|
let onClose: () -> Void
|
||||||
|
|
||||||
|
private let info = ArtefactFiles.appInfo()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Image("ICCery-logo")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(height: 64)
|
||||||
|
|
||||||
|
Text("ICCery")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text("Version:")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(info.version)
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.accessibilityIdentifier("aboutVersion")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("Build:")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(info.build)
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("Build date:")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(info.buildDate)
|
||||||
|
.foregroundStyle(Theme.text)
|
||||||
|
.accessibilityIdentifier("aboutBuildDate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.callout)
|
||||||
|
|
||||||
|
Text("Native macOS printer profiling workstation.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
Button("Close") {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
.controlSize(.large)
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
.accessibilityIdentifier("closeAboutBtn")
|
||||||
|
}
|
||||||
|
.padding(32)
|
||||||
|
.frame(width: 360)
|
||||||
|
.background(Theme.panel)
|
||||||
|
.accessibilityIdentifier("aboutDialog")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Reusable help overlay badge that does not reflow layout (#171).
|
||||||
|
///
|
||||||
|
/// When `showing` is `true`, a small indicator is rendered as an overlay at the
|
||||||
|
/// top-trailing corner of the wrapped view. The native `.help` tooltip is always
|
||||||
|
/// available on hover, so the overlay is purely a visual cue in help mode.
|
||||||
|
struct HelpOverlay: ViewModifier {
|
||||||
|
let text: String
|
||||||
|
@Binding var showing: Bool
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.help(text)
|
||||||
|
.overlay(alignment: .topTrailing) {
|
||||||
|
if showing {
|
||||||
|
Image(systemName: "questionmark.circle.fill")
|
||||||
|
.font(.system(size: 10, weight: .bold))
|
||||||
|
.foregroundStyle(Theme.accent)
|
||||||
|
.offset(x: 8, y: -8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
/// Adds a non-reflowing help overlay to the view.
|
||||||
|
func helpOverlay(_ text: String, showing: Binding<Bool>) -> some View {
|
||||||
|
modifier(HelpOverlay(text: text, showing: showing))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ struct RootView: View {
|
|||||||
@Bindable var workflow: TargetWorkflowViewModel
|
@Bindable var workflow: TargetWorkflowViewModel
|
||||||
@State private var showingSettings = false
|
@State private var showingSettings = false
|
||||||
@State private var showingAbout = false
|
@State private var showingAbout = false
|
||||||
|
@State private var showingAllHelp = false
|
||||||
|
|
||||||
private var model: WizardViewModel { workflow.wizard }
|
private var model: WizardViewModel { workflow.wizard }
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ struct RootView: View {
|
|||||||
SidebarView(
|
SidebarView(
|
||||||
workflow: workflow,
|
workflow: workflow,
|
||||||
onOpenSettings: { showingSettings = true },
|
onOpenSettings: { showingSettings = true },
|
||||||
onOpenAbout: { showingAbout = true }
|
onOpenAbout: { showingAbout = true },
|
||||||
|
showingAllHelp: $showingAllHelp
|
||||||
)
|
)
|
||||||
|
|
||||||
Rectangle()
|
Rectangle()
|
||||||
@@ -48,10 +50,8 @@ struct RootView: View {
|
|||||||
.sheet(isPresented: $workflow.showingManagePresets) {
|
.sheet(isPresented: $workflow.showingManagePresets) {
|
||||||
ManagePresetsDialog(workflow: workflow)
|
ManagePresetsDialog(workflow: workflow)
|
||||||
}
|
}
|
||||||
.alert("ICCery 2.0.0", isPresented: $showingAbout) {
|
.sheet(isPresented: $showingAbout) {
|
||||||
Button("OK") {}
|
AboutView { showingAbout = false }
|
||||||
} message: {
|
|
||||||
Text("Native macOS printer profiling workstation.\nFull About dialog lands in issue #31.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ struct SidebarView: View {
|
|||||||
@Bindable var workflow: TargetWorkflowViewModel
|
@Bindable var workflow: TargetWorkflowViewModel
|
||||||
var onOpenSettings: () -> Void
|
var onOpenSettings: () -> Void
|
||||||
var onOpenAbout: () -> Void
|
var onOpenAbout: () -> Void
|
||||||
|
@Binding var showingAllHelp: Bool
|
||||||
|
|
||||||
private var model: WizardViewModel { workflow.wizard }
|
private var model: WizardViewModel { workflow.wizard }
|
||||||
|
|
||||||
@@ -22,12 +23,20 @@ struct SidebarView: View {
|
|||||||
Image(systemName: "gearshape")
|
Image(systemName: "gearshape")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.help("Settings")
|
.helpOverlay("Open the Settings dialog.", showing: $showingAllHelp)
|
||||||
|
.accessibilityIdentifier("openSettingsBtn")
|
||||||
Button(action: onOpenAbout) {
|
Button(action: onOpenAbout) {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.help("About ICCery")
|
.helpOverlay("Open the About dialog.", showing: $showingAllHelp)
|
||||||
|
.accessibilityIdentifier("openAboutBtn")
|
||||||
|
Button(action: { showingAllHelp.toggle() }) {
|
||||||
|
Image(systemName: showingAllHelp ? "questionmark.circle.fill" : "questionmark.circle")
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Toggle help overlays")
|
||||||
|
.accessibilityIdentifier("btnToggleAllHelp")
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
|
/// About and help chrome UI tests (issue #31).
|
||||||
|
@MainActor
|
||||||
|
final class AboutHelpUITests: XCTestCase {
|
||||||
|
|
||||||
|
private var app: XCUIApplication!
|
||||||
|
|
||||||
|
override func setUp() async throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
app = XCUIApplication()
|
||||||
|
app.launchEnvironment = ["ICCERY_UI_TESTING": "1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() async throws {
|
||||||
|
app?.terminate()
|
||||||
|
app = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func element(_ id: String) -> XCUIElement {
|
||||||
|
app.descendants(matching: .any)[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func waitFor(_ id: String, timeout: TimeInterval = 10) -> XCUIElement {
|
||||||
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
while Date() < deadline {
|
||||||
|
let el = element(id)
|
||||||
|
if el.exists { return el }
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
||||||
|
}
|
||||||
|
let el = element(id)
|
||||||
|
XCTAssertTrue(el.exists, "Expected element \(id)")
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAboutDialogShowsVersionAndBuildDate() throws {
|
||||||
|
app.launch()
|
||||||
|
app.activate()
|
||||||
|
|
||||||
|
let openAbout = app.buttons["openAboutBtn"]
|
||||||
|
XCTAssertTrue(openAbout.waitForExistence(timeout: 10))
|
||||||
|
openAbout.click()
|
||||||
|
|
||||||
|
_ = waitFor("aboutDialog", timeout: 10)
|
||||||
|
XCTAssertTrue(element("aboutVersion").exists)
|
||||||
|
XCTAssertTrue(element("aboutBuildDate").exists)
|
||||||
|
|
||||||
|
let close = app.buttons["closeAboutBtn"]
|
||||||
|
XCTAssertTrue(close.exists)
|
||||||
|
close.click()
|
||||||
|
|
||||||
|
XCTAssertFalse(element("aboutDialog").exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHelpOverlaysDoNotChangeSidebarHeight() throws {
|
||||||
|
app.launch()
|
||||||
|
app.activate()
|
||||||
|
|
||||||
|
let toggle = app.buttons["btnToggleAllHelp"]
|
||||||
|
XCTAssertTrue(toggle.waitForExistence(timeout: 10))
|
||||||
|
|
||||||
|
let sidebar = app.groups.containing(.button, identifier: "openSettingsBtn").element
|
||||||
|
let before = sidebar.frame
|
||||||
|
|
||||||
|
toggle.click()
|
||||||
|
let after = sidebar.frame
|
||||||
|
|
||||||
|
XCTAssertEqual(before.size.height, after.size.height,
|
||||||
|
"Toggling global help must not reflow the sidebar height.")
|
||||||
|
XCTAssertTrue(app.descendants(matching: .any)["openSettingsBtn"].exists)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user