From 74f3adc1239e43e637c41d28197a9878c04e085e Mon Sep 17 00:00:00 2001 From: Raffe Yang Date: Mon, 12 Oct 2026 10:37:03 +0800 Subject: [PATCH 1/2] feat(settings): hide the Welcome window version and update check Adds showWelcomeVersionInfo to GeneralSettings, on by default, with a decode fallback so older settings blobs keep it on. A toggle in General > Software Update controls it, and the welcome window reads it live: off drops the version line and the Check for Updates link. --- CHANGELOG.md | 4 ++ .../Models/Settings/GeneralSettings.swift | 7 ++++ .../Views/Settings/GeneralSettingsView.swift | 4 ++ .../Views/Welcome/WelcomeActionsPanel.swift | 19 +++++---- .../Models/GeneralSettingsTests.swift | 24 +++++++++++ .../SoftwareUpdateSettingsUITests.swift | 1 + TableProUITests/WelcomeWindowUITests.swift | 42 +++++++++++++++++++ docs/customization/general-settings.mdx | 2 + 8 files changed, 95 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6950de15ce..a4be9cd5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- An option in General Settings to hide the version line and Check for Updates on the Welcome window. + ## [0.80.0] - 2026-10-11 Google Cloud IAM sign-in for MySQL and PostgreSQL on Cloud SQL, with no Cloud SQL Auth Proxy. diff --git a/TablePro/Models/Settings/GeneralSettings.swift b/TablePro/Models/Settings/GeneralSettings.swift index cd7b8de6b0..3a866db90b 100644 --- a/TablePro/Models/Settings/GeneralSettings.swift +++ b/TablePro/Models/Settings/GeneralSettings.swift @@ -84,6 +84,9 @@ struct GeneralSettings: Codable, Equatable { /// Whether the window shows the workspace rail listing every open connection and database var showWorkspaceRail: Bool + /// Whether the welcome window shows the app version line and the Check for Updates link under it + var showWelcomeVersionInfo: Bool + /// How tall sidebar rows are drawn, following the system Appearance setting unless overridden var sidebarRowSize: SidebarRowSizePreference @@ -101,6 +104,7 @@ struct GeneralSettings: Codable, Equatable { showSystemContainers: false, showPartitions: true, showWorkspaceRail: true, + showWelcomeVersionInfo: true, sidebarRowSize: .matchSystem, connectionHealthCheck: .every30Seconds ) @@ -116,6 +120,7 @@ struct GeneralSettings: Codable, Equatable { showSystemContainers: Bool = false, showPartitions: Bool = true, showWorkspaceRail: Bool = true, + showWelcomeVersionInfo: Bool = true, sidebarRowSize: SidebarRowSizePreference = .matchSystem, connectionHealthCheck: ConnectionHealthCheck = .every30Seconds ) { @@ -129,6 +134,7 @@ struct GeneralSettings: Codable, Equatable { self.showSystemContainers = showSystemContainers self.showPartitions = showPartitions self.showWorkspaceRail = showWorkspaceRail + self.showWelcomeVersionInfo = showWelcomeVersionInfo self.sidebarRowSize = sidebarRowSize self.connectionHealthCheck = connectionHealthCheck } @@ -145,6 +151,7 @@ struct GeneralSettings: Codable, Equatable { showSystemContainers = try container.decodeIfPresent(Bool.self, forKey: .showSystemContainers) ?? false showPartitions = try container.decodeIfPresent(Bool.self, forKey: .showPartitions) ?? true showWorkspaceRail = try container.decodeIfPresent(Bool.self, forKey: .showWorkspaceRail) ?? true + showWelcomeVersionInfo = try container.decodeIfPresent(Bool.self, forKey: .showWelcomeVersionInfo) ?? true sidebarRowSize = try container.decodeIfPresent( SidebarRowSizePreference.self, forKey: .sidebarRowSize ) ?? .matchSystem diff --git a/TablePro/Views/Settings/GeneralSettingsView.swift b/TablePro/Views/Settings/GeneralSettingsView.swift index b8e796f90c..62e5f1a2b8 100644 --- a/TablePro/Views/Settings/GeneralSettingsView.swift +++ b/TablePro/Views/Settings/GeneralSettingsView.swift @@ -151,6 +151,10 @@ struct GeneralSettingsView: View { TrustedExternalConnectionsSection() Section { + Toggle("Show version and updates on the Welcome window", isOn: $settings.showWelcomeVersionInfo) + .accessibilityIdentifier("welcome-version-info-toggle") + .help("Shows the app version and the Check for Updates link under the app icon on the Welcome window. Turn it off to keep the welcome window plain.") + Toggle("Automatically check for updates", isOn: automaticallyChecksForUpdates) .accessibilityIdentifier("automatic-update-check-toggle") diff --git a/TablePro/Views/Welcome/WelcomeActionsPanel.swift b/TablePro/Views/Welcome/WelcomeActionsPanel.swift index 71aa6a2224..99705e7bdc 100644 --- a/TablePro/Views/Welcome/WelcomeActionsPanel.swift +++ b/TablePro/Views/Welcome/WelcomeActionsPanel.swift @@ -7,6 +7,7 @@ import SwiftUI struct WelcomeActionsPanel: View { @ObservedObject private var licenseManager = LicenseManager.shared + @ObservedObject private var settingsManager = AppSettingsManager.shared let onActivateLicense: () -> Void let onNewConnection: () -> Void let onOpenFile: () -> Void @@ -35,16 +36,18 @@ struct WelcomeActionsPanel: View { Text(verbatim: "TablePro") .font(.title2.weight(.semibold)) - Text(String(format: String(localized: "Version %@"), Bundle.main.appVersion)) - .font(.callout) - .foregroundStyle(.secondary) + if settingsManager.general.showWelcomeVersionInfo { + Text(String(format: String(localized: "Version %@"), Bundle.main.appVersion)) + .font(.callout) + .foregroundStyle(.secondary) - Button(updater.checkForUpdatesTitle) { - updater.checkForUpdates() + Button(updater.checkForUpdatesTitle) { + updater.checkForUpdates() + } + .buttonStyle(.link) + .font(.callout) + .disabled(!updater.canCheckForUpdates) } - .buttonStyle(.link) - .font(.callout) - .disabled(!updater.canCheckForUpdates) if showsWhatsNew { Button(String(format: String(localized: "What's New in %@"), Bundle.main.appVersion)) { diff --git a/TableProTests/Models/GeneralSettingsTests.swift b/TableProTests/Models/GeneralSettingsTests.swift index e5919cb720..f7b9776f21 100644 --- a/TableProTests/Models/GeneralSettingsTests.swift +++ b/TableProTests/Models/GeneralSettingsTests.swift @@ -203,3 +203,27 @@ struct GeneralSettingsPartitionsTests { #expect(decoded.showPartitions == false) } } + +struct GeneralSettingsWelcomeVersionInfoTests { + @Test("Defaults to on") + func defaultsOn() { + #expect(GeneralSettings.default.showWelcomeVersionInfo == true) + #expect(GeneralSettings().showWelcomeVersionInfo == true) + } + + @Test("Settings saved before the key existed keep the version line on the welcome window") + func decodesMissingKeyAsOn() throws { + let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8) + let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json) + #expect(decoded.showWelcomeVersionInfo == true) + } + + @Test("Round-trips when turned off") + func roundTripsDisabled() throws { + var settings = GeneralSettings() + settings.showWelcomeVersionInfo = false + let data = try JSONEncoder().encode(settings) + let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) + #expect(decoded.showWelcomeVersionInfo == false) + } +} diff --git a/TableProUITests/SoftwareUpdateSettingsUITests.swift b/TableProUITests/SoftwareUpdateSettingsUITests.swift index 0b1bb30a2e..7d44e6b829 100644 --- a/TableProUITests/SoftwareUpdateSettingsUITests.swift +++ b/TableProUITests/SoftwareUpdateSettingsUITests.swift @@ -17,6 +17,7 @@ final class SoftwareUpdateSettingsUITests: UITestCase { let app = try launchApp() openGeneralSettings(in: app) + XCTAssertTrue(app.switches["welcome-version-info-toggle"].firstMatch.waitToExist(timeout: 10)) XCTAssertTrue(app.switches["automatic-update-check-toggle"].firstMatch.waitToExist(timeout: 10)) XCTAssertTrue(app.switches["automatic-update-install-toggle"].firstMatch.waitToExist(timeout: 10)) XCTAssertTrue(app.buttons["check-for-updates-button"].firstMatch.waitToExist(timeout: 10)) diff --git a/TableProUITests/WelcomeWindowUITests.swift b/TableProUITests/WelcomeWindowUITests.swift index 7d2170d8a3..53deda8db5 100644 --- a/TableProUITests/WelcomeWindowUITests.swift +++ b/TableProUITests/WelcomeWindowUITests.swift @@ -1,3 +1,4 @@ +import TableProPluginKit import XCTest final class WelcomeWindowUITests: UITestCase { @@ -53,4 +54,45 @@ final class WelcomeWindowUITests: UITestCase { XCTAssertTrue(waitForSampleDatabaseWindow(in: app), "Open Sample Database must open the sample") } + + func testTheWelcomeWindowShowsTheVersionAndTheUpdateCheck() throws { + let app = try launchApp() + let welcome = app.windows["welcome"] + XCTAssertTrue(welcome.waitToExist(timeout: 10)) + + XCTAssertTrue( + welcome.links["Check for Updates…"].waitToExist(timeout: 10), + "The welcome window must offer the update check by default" + ) + let versionLine = welcome.staticTexts + .matching(NSPredicate(format: "label BEGINSWITH %@", "Version")) + .firstMatch + XCTAssertTrue(versionLine.waitToExist(timeout: 10), "The welcome window must show the app version") + } + + /// The setting is seeded straight into the suite the app reads rather than clicked in Settings, + /// because the toggle sits far enough down the General pane that the runner's screen needs the + /// pane scrolled to reach it. + func testTurningTheSettingOffHidesTheVersionAndTheUpdateCheck() throws { + let seeded = try XCTUnwrap(UserDefaults(suiteName: PluginHostStorage.sandboxSuiteName)) + seeded.set( + Data(#"{"startupBehavior":"showWelcome","showWelcomeVersionInfo":false}"#.utf8), + forKey: "com.TablePro.settings.general" + ) + + let app = try launchApp() + let welcome = app.windows["welcome"] + XCTAssertTrue(welcome.waitToExist(timeout: 10)) + // The panel must have mounted before an absence means anything. + XCTAssertTrue(welcome.buttons["New Connection…"].waitToExist(timeout: 10)) + + let versionLine = welcome.staticTexts + .matching(NSPredicate(format: "label BEGINSWITH %@", "Version")) + .firstMatch + let updateCheck = welcome.links["Check for Updates…"] + XCTAssertTrue( + waitForPredicate(timeout: 5) { !versionLine.exists && !updateCheck.exists }, + "A welcome window with the version info off must show neither the app version nor the update check" + ) + } } diff --git a/docs/customization/general-settings.mdx b/docs/customization/general-settings.mdx index a4c7e263bf..68f038a006 100644 --- a/docs/customization/general-settings.mdx +++ b/docs/customization/general-settings.mdx @@ -64,6 +64,8 @@ External links you chose to always allow, each with **Forget**, plus **Forget Al Both switches are on out of the box, and together they are the whole of the decision: the first one stops the daily check, the second stops the install. +**Show version and updates on the Welcome window** places the version line and the Check for Updates link under the app icon on the welcome window. Turn it off for a plainer welcome window; **TablePro > Check for Updates…** in the menu bar and the button on this tab still check. + **Automatically check for updates** runs the check in the background once a day. Turn it off and nothing reaches the network unless you press the button. **Download and install updates automatically** fetches the new version in the background and swaps it in the next time TablePro quits, with no dialog. Turn it off to be asked first. With the check switched off the switch is dimmed and installs never run. From bdb7f9dd8f894fe5fbac5d211b5b4e401392abf4 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 12 Oct 2026 22:38:19 +0700 Subject: [PATCH 2/2] refactor(settings): rework the welcome version toggle after review --- CHANGELOG.md | 2 +- .../Models/Settings/GeneralSettings.swift | 25 +-- TablePro/Resources/Localizable.xcstrings | 80 ++++++++ .../Views/Settings/GeneralSettingsView.swift | 8 +- .../Views/Welcome/WelcomeActionsPanel.swift | 18 +- .../Models/GeneralSettingsTests.swift | 180 ++++-------------- .../SoftwareUpdateSettingsUITests.swift | 1 - TableProUITests/WelcomeWindowUITests.swift | 60 +++--- 8 files changed, 168 insertions(+), 206 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0edebcf8..37fcff54ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Panel and status colors in the theme editor, covering the results, inspector, structure, compare and query plan panes. - Themes that name a system color for a slot, so the built-in themes keep the system's own contrast settings. - Reason shown in Settings > Appearance for a theme file that could not be loaded. -- An option in General Settings to hide the version line and Check for Updates on the Welcome window. +- Setting in Settings > General to hide the version and Check for Updates on the Welcome window. - Install Parquet… in the Export dialog's format menu when the Parquet plugin is not installed. ### Changed diff --git a/TablePro/Models/Settings/GeneralSettings.swift b/TablePro/Models/Settings/GeneralSettings.swift index 30ffb50d89..ea85b06714 100644 --- a/TablePro/Models/Settings/GeneralSettings.swift +++ b/TablePro/Models/Settings/GeneralSettings.swift @@ -5,7 +5,6 @@ import Foundation -/// Startup behavior when app launches enum StartupBehavior: String, Codable, CaseIterable, Identifiable { case showWelcome = "showWelcome" case reopenLast = "reopenLast" @@ -20,7 +19,6 @@ enum StartupBehavior: String, Codable, CaseIterable, Identifiable { } } -/// App language options enum AppLanguage: String, Codable, CaseIterable, Identifiable { case system = "system" case english = "en" @@ -55,42 +53,21 @@ enum AppLanguage: String, Codable, CaseIterable, Identifiable { } } -/// General app settings struct GeneralSettings: Codable, Equatable { var startupBehavior: StartupBehavior var language: AppLanguage - /// Query execution timeout in seconds (0 = no limit) + // 0 means no limit var queryTimeoutSeconds: Int - - /// Whether to share anonymous usage analytics var shareAnalytics: Bool - - /// Whether the sidebar shows a Recent section with recently opened tables var showRecentTables: Bool - - /// Whether to show database object comments in the sidebar and data grid headers var showObjectComments: Bool - - /// Whether sidebar rows show a type icon before the object name var showObjectIcons: Bool - - /// Whether the sidebar tree and its database filter list system databases and schemas var showSystemContainers: Bool - - /// Whether a partitioned table lists its partitions in the sidebar and shows how many it holds var showPartitions: Bool - - /// Whether the window shows the workspace rail listing every open connection and database var showWorkspaceRail: Bool - - /// Whether the welcome window shows the app version line and the Check for Updates link under it var showWelcomeVersionInfo: Bool - - /// How tall sidebar rows are drawn, following the system Appearance setting unless overridden var sidebarRowSize: SidebarRowSizePreference - - /// How often TablePro checks that an open connection still works var connectionHealthCheck: ConnectionHealthCheck static let `default` = GeneralSettings( diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 62e4f13e12..e782cbe2e0 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -161773,6 +161773,46 @@ } } }, + "Show version and updates on the Welcome window" : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher la version et les mises à jour dans la fenêtre d’accueil" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시작 윈도우에 버전 및 업데이트 보기" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karşılama penceresinde sürümü ve güncellemeleri göster" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện phiên bản và cập nhật trên cửa sổ chào mừng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在欢迎窗口显示版本和更新" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在歡迎視窗顯示版本和更新" + } + } + } + }, "Show Welcome Screen" : { "localizations" : { "fr" : { @@ -162411,6 +162451,46 @@ } } }, + "Shows the version and Check for Updates under the app icon. An available update is shown either way." : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Affiche la version et Rechercher les mises à jour sous l’icône de l’app. Une mise à jour disponible s’affiche dans tous les cas." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "앱 아이콘 아래에 버전과 업데이트 확인을 표시합니다. 사용 가능한 업데이트는 어느 경우에나 표시됩니다." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama simgesinin altında sürümü ve Güncellemeleri Kontrol Et bağlantısını gösterir. Mevcut bir güncelleme her durumda gösterilir." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện phiên bản và Kiểm tra cập nhật dưới biểu tượng ứng dụng. Bản cập nhật có sẵn vẫn luôn được hiện." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在应用图标下方显示版本和“检查更新”。有可用更新时始终会显示。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 App 圖示下方顯示版本和「檢查更新」。有可用更新時一律會顯示。" + } + } + } + }, "Side" : { "localizations" : { "fr" : { diff --git a/TablePro/Views/Settings/GeneralSettingsView.swift b/TablePro/Views/Settings/GeneralSettingsView.swift index 62e5f1a2b8..45c530cd9c 100644 --- a/TablePro/Views/Settings/GeneralSettingsView.swift +++ b/TablePro/Views/Settings/GeneralSettingsView.swift @@ -76,6 +76,10 @@ struct GeneralSettingsView: View { } } + Toggle("Show version and updates on the Welcome window", isOn: $settings.showWelcomeVersionInfo) + .accessibilityIdentifier("welcome-version-info-toggle") + .help("Shows the version and Check for Updates under the app icon. An available update is shown either way.") + Section("Tabs") { Toggle("Enable preview tabs", isOn: $tabSettings.enablePreviewTabs) .help("Single-clicking a table opens a temporary tab that gets replaced on next click.") @@ -151,10 +155,6 @@ struct GeneralSettingsView: View { TrustedExternalConnectionsSection() Section { - Toggle("Show version and updates on the Welcome window", isOn: $settings.showWelcomeVersionInfo) - .accessibilityIdentifier("welcome-version-info-toggle") - .help("Shows the app version and the Check for Updates link under the app icon on the Welcome window. Turn it off to keep the welcome window plain.") - Toggle("Automatically check for updates", isOn: automaticallyChecksForUpdates) .accessibilityIdentifier("automatic-update-check-toggle") diff --git a/TablePro/Views/Welcome/WelcomeActionsPanel.swift b/TablePro/Views/Welcome/WelcomeActionsPanel.swift index 99705e7bdc..139cb053ed 100644 --- a/TablePro/Views/Welcome/WelcomeActionsPanel.swift +++ b/TablePro/Views/Welcome/WelcomeActionsPanel.swift @@ -3,11 +3,12 @@ // TablePro // +import Combine import SwiftUI struct WelcomeActionsPanel: View { @ObservedObject private var licenseManager = LicenseManager.shared - @ObservedObject private var settingsManager = AppSettingsManager.shared + @ObservedObject private var updater = SoftwareUpdater.shared let onActivateLicense: () -> Void let onNewConnection: () -> Void let onOpenFile: () -> Void @@ -17,7 +18,12 @@ struct WelcomeActionsPanel: View { let onImportConnectionsFile: () -> Void let onOpenProjectFolder: () -> Void - private let updater = SoftwareUpdater.shared + // One key, not the whole manager: the window lives all session and every settings write would redraw it. + @State private var showsVersionInfo = AppSettingsManager.shared.general.showWelcomeVersionInfo + + private static let versionInfoSetting = AppSettingsManager.shared.$general + .map(\.showWelcomeVersionInfo) + .removeDuplicates() /// Captured once, because the stored value is overwritten on the same appearance that reads /// it. Without the capture the line would replace itself with nothing on the next redraw. @@ -36,17 +42,22 @@ struct WelcomeActionsPanel: View { Text(verbatim: "TablePro") .font(.title2.weight(.semibold)) - if settingsManager.general.showWelcomeVersionInfo { + if showsVersionInfo { Text(String(format: String(localized: "Version %@"), Bundle.main.appVersion)) .font(.callout) .foregroundStyle(.secondary) + .accessibilityIdentifier("welcome-version-line") + } + // A deferred update has its gentle reminder here, so hiding the line never hides that. + if showsVersionInfo || updater.hasPendingUpdate { Button(updater.checkForUpdatesTitle) { updater.checkForUpdates() } .buttonStyle(.link) .font(.callout) .disabled(!updater.canCheckForUpdates) + .accessibilityIdentifier("welcome-check-for-updates-link") } if showsWhatsNew { @@ -106,6 +117,7 @@ struct WelcomeActionsPanel: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear(perform: recordAppVersion) + .onReceive(Self.versionInfoSetting) { showsVersionInfo = $0 } } private func recordAppVersion() { diff --git a/TableProTests/Models/GeneralSettingsTests.swift b/TableProTests/Models/GeneralSettingsTests.swift index f7b9776f21..7d47fb8710 100644 --- a/TableProTests/Models/GeneralSettingsTests.swift +++ b/TableProTests/Models/GeneralSettingsTests.swift @@ -54,109 +54,57 @@ struct AppLanguageTests { } } -struct GeneralSettingsTests { - @Test("Defaults to off") - func defaultsOff() { - #expect(GeneralSettings.default.showRecentTables == false) - #expect(GeneralSettings().showRecentTables == false) - } - - @Test("Decoding settings without the key keeps recent tables off") - func decodesMissingKeyAsOff() throws { - let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json) - #expect(decoded.showRecentTables == false) - } - - @Test("Round-trips when enabled") - func roundTripsEnabled() throws { - var settings = GeneralSettings() - settings.showRecentTables = true - let data = try JSONEncoder().encode(settings) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) - #expect(decoded.showRecentTables == true) - } -} - -struct GeneralSettingsObjectIconsTests { - @Test("Defaults to on") - func defaultsOn() { - #expect(GeneralSettings.default.showObjectIcons == true) - #expect(GeneralSettings().showObjectIcons == true) - } - - @Test("Decoding settings saved before the key existed keeps icons on") - func decodesMissingKeyAsOn() throws { - let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json) - #expect(decoded.showObjectIcons == true) - } - - @Test("Round-trips when disabled") - func roundTripsDisabled() throws { - var settings = GeneralSettings() - settings.showObjectIcons = false +struct GeneralSettingsBoolSettingTests { + struct BoolSetting: Sendable, CustomTestStringConvertible { + let key: String + let defaultValue: Bool + + var testDescription: String { key } + } + + static let settings = [ + BoolSetting(key: "shareAnalytics", defaultValue: true), + BoolSetting(key: "showRecentTables", defaultValue: false), + BoolSetting(key: "showObjectComments", defaultValue: true), + BoolSetting(key: "showObjectIcons", defaultValue: true), + BoolSetting(key: "showSystemContainers", defaultValue: false), + BoolSetting(key: "showPartitions", defaultValue: true), + BoolSetting(key: "showWorkspaceRail", defaultValue: true), + BoolSetting(key: "showWelcomeVersionInfo", defaultValue: true) + ] + + private static func encoded(_ settings: GeneralSettings) throws -> [String: Any] { let data = try JSONEncoder().encode(settings) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) - #expect(decoded.showObjectIcons == false) + return try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any]) } - @Test("Icons and comments are independent") - func independentFromComments() throws { - var settings = GeneralSettings() - settings.showObjectIcons = false - let data = try JSONEncoder().encode(settings) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) - #expect(decoded.showObjectIcons == false) - #expect(decoded.showObjectComments == true) + private static func decoded(_ object: [String: Any]) throws -> GeneralSettings { + try JSONDecoder().decode(GeneralSettings.self, from: JSONSerialization.data(withJSONObject: object)) } -} -struct GeneralSettingsWorkspaceRailTests { - @Test("Defaults to on") - func defaultsOn() { - #expect(GeneralSettings.default.showWorkspaceRail) - #expect(GeneralSettings().showWorkspaceRail) + @Test("Starts at its default", arguments: settings) + func startsAtDefault(_ setting: BoolSetting) throws { + #expect(try Self.encoded(.default)[setting.key] as? Bool == setting.defaultValue) + #expect(try Self.encoded(GeneralSettings())[setting.key] as? Bool == setting.defaultValue) } - @Test("Settings saved before the rail existed keep it on") - func decodesMissingKeyAsOn() throws { - let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json) - #expect(decoded.showWorkspaceRail) - } - - @Test("Round-trips when turned off") - func roundTripsDisabled() throws { - var settings = GeneralSettings() - settings.showWorkspaceRail = false - let data = try JSONEncoder().encode(settings) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) - #expect(!decoded.showWorkspaceRail) + @Test("Settings saved before the key existed decode to the default", arguments: settings) + func decodesMissingKeyAsDefault(_ setting: BoolSetting) throws { + let decoded = try Self.decoded(["startupBehavior": "showWelcome"]) + #expect(try Self.encoded(decoded)[setting.key] as? Bool == setting.defaultValue) } -} -struct GeneralSettingsSystemContainersTests { - @Test("Defaults to off") - func defaultsOff() { - #expect(GeneralSettings.default.showSystemContainers == false) - #expect(GeneralSettings().showSystemContainers == false) - } + @Test("Round-trips the other value and leaves every other setting alone", arguments: settings) + func roundTripsTheOtherValue(_ setting: BoolSetting) throws { + var object = try Self.encoded(.default) + object[setting.key] = !setting.defaultValue - @Test("Settings saved before the key existed keep system databases hidden in the sidebar") - func decodesMissingKeyAsOff() throws { - let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json) - #expect(decoded.showSystemContainers == false) - } + let roundTripped = try Self.encoded(Self.decoded(object)) - @Test("Round-trips when turned on") - func roundTripsEnabled() throws { - var settings = GeneralSettings() - settings.showSystemContainers = true - let data = try JSONEncoder().encode(settings) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) - #expect(decoded.showSystemContainers) + #expect(roundTripped[setting.key] as? Bool == !setting.defaultValue) + for other in Self.settings where other.key != setting.key { + #expect(roundTripped[other.key] as? Bool == other.defaultValue, "\(other.key)") + } } } @@ -179,51 +127,3 @@ struct GeneralSettingsUpdatePreferenceTests { #expect(object["automaticallyCheckForUpdates"] == nil) } } - -struct GeneralSettingsPartitionsTests { - @Test("Defaults to on") - func defaultsOn() { - #expect(GeneralSettings.default.showPartitions == true) - #expect(GeneralSettings().showPartitions == true) - } - - @Test("Settings saved before the key existed keep partitions listed") - func decodesMissingKeyAsOn() throws { - let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json) - #expect(decoded.showPartitions == true) - } - - @Test("Round-trips when turned off") - func roundTripsDisabled() throws { - var settings = GeneralSettings() - settings.showPartitions = false - let data = try JSONEncoder().encode(settings) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) - #expect(decoded.showPartitions == false) - } -} - -struct GeneralSettingsWelcomeVersionInfoTests { - @Test("Defaults to on") - func defaultsOn() { - #expect(GeneralSettings.default.showWelcomeVersionInfo == true) - #expect(GeneralSettings().showWelcomeVersionInfo == true) - } - - @Test("Settings saved before the key existed keep the version line on the welcome window") - func decodesMissingKeyAsOn() throws { - let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json) - #expect(decoded.showWelcomeVersionInfo == true) - } - - @Test("Round-trips when turned off") - func roundTripsDisabled() throws { - var settings = GeneralSettings() - settings.showWelcomeVersionInfo = false - let data = try JSONEncoder().encode(settings) - let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data) - #expect(decoded.showWelcomeVersionInfo == false) - } -} diff --git a/TableProUITests/SoftwareUpdateSettingsUITests.swift b/TableProUITests/SoftwareUpdateSettingsUITests.swift index 7d44e6b829..0b1bb30a2e 100644 --- a/TableProUITests/SoftwareUpdateSettingsUITests.swift +++ b/TableProUITests/SoftwareUpdateSettingsUITests.swift @@ -17,7 +17,6 @@ final class SoftwareUpdateSettingsUITests: UITestCase { let app = try launchApp() openGeneralSettings(in: app) - XCTAssertTrue(app.switches["welcome-version-info-toggle"].firstMatch.waitToExist(timeout: 10)) XCTAssertTrue(app.switches["automatic-update-check-toggle"].firstMatch.waitToExist(timeout: 10)) XCTAssertTrue(app.switches["automatic-update-install-toggle"].firstMatch.waitToExist(timeout: 10)) XCTAssertTrue(app.buttons["check-for-updates-button"].firstMatch.waitToExist(timeout: 10)) diff --git a/TableProUITests/WelcomeWindowUITests.swift b/TableProUITests/WelcomeWindowUITests.swift index 53deda8db5..0f6d7a854e 100644 --- a/TableProUITests/WelcomeWindowUITests.swift +++ b/TableProUITests/WelcomeWindowUITests.swift @@ -1,4 +1,3 @@ -import TableProPluginKit import XCTest final class WelcomeWindowUITests: UITestCase { @@ -55,44 +54,39 @@ final class WelcomeWindowUITests: UITestCase { XCTAssertTrue(waitForSampleDatabaseWindow(in: app), "Open Sample Database must open the sample") } - func testTheWelcomeWindowShowsTheVersionAndTheUpdateCheck() throws { - let app = try launchApp() - let welcome = app.windows["welcome"] + func testTheVersionSettingHidesTheVersionAndTheUpdateCheckOnTheOpenWindow() throws { + let app = try launchApp(arguments: ["-AppleLanguages", "(en)"]) + let welcome = app.children(matching: .window).matching(identifier: "welcome").firstMatch XCTAssertTrue(welcome.waitToExist(timeout: 10)) + let versionLine = welcome.descendants(matching: .any).matching(identifier: "welcome-version-line").firstMatch + let updateCheck = welcome.descendants(matching: .any) + .matching(identifier: "welcome-check-for-updates-link").firstMatch + XCTAssertTrue(versionLine.waitToExist(timeout: 10), "The welcome window must show the version by default") + XCTAssertTrue(updateCheck.exists, "The welcome window must offer the update check by default") + + app.menuBars.menuItems["Settings…"].click() + let settings = app.children(matching: .window).matching(identifier: "settings").firstMatch + XCTAssertTrue(settings.waitToExist(timeout: 10), "Settings must open") + let generalPaneButton = settings.toolbars.buttons["General"] + XCTAssertTrue(generalPaneButton.waitToExist(timeout: 10)) + generalPaneButton.click() + + let toggle = settings.descendants(matching: .any) + .matching(identifier: "welcome-version-info-toggle").firstMatch + XCTAssertTrue(waitUntilHittable(toggle, timeout: 10), "The General pane must offer the setting") + XCTAssertTrue(isOn(toggle), "The setting must start on") + + toggle.click() XCTAssertTrue( - welcome.links["Check for Updates…"].waitToExist(timeout: 10), - "The welcome window must offer the update check by default" - ) - let versionLine = welcome.staticTexts - .matching(NSPredicate(format: "label BEGINSWITH %@", "Version")) - .firstMatch - XCTAssertTrue(versionLine.waitToExist(timeout: 10), "The welcome window must show the app version") - } - - /// The setting is seeded straight into the suite the app reads rather than clicked in Settings, - /// because the toggle sits far enough down the General pane that the runner's screen needs the - /// pane scrolled to reach it. - func testTurningTheSettingOffHidesTheVersionAndTheUpdateCheck() throws { - let seeded = try XCTUnwrap(UserDefaults(suiteName: PluginHostStorage.sandboxSuiteName)) - seeded.set( - Data(#"{"startupBehavior":"showWelcome","showWelcomeVersionInfo":false}"#.utf8), - forKey: "com.TablePro.settings.general" + waitForPredicate(timeout: 10) { !versionLine.exists && !updateCheck.exists }, + "Turning the setting off must hide the version and the update check on the open welcome window" ) - let app = try launchApp() - let welcome = app.windows["welcome"] - XCTAssertTrue(welcome.waitToExist(timeout: 10)) - // The panel must have mounted before an absence means anything. - XCTAssertTrue(welcome.buttons["New Connection…"].waitToExist(timeout: 10)) - - let versionLine = welcome.staticTexts - .matching(NSPredicate(format: "label BEGINSWITH %@", "Version")) - .firstMatch - let updateCheck = welcome.links["Check for Updates…"] + toggle.click() XCTAssertTrue( - waitForPredicate(timeout: 5) { !versionLine.exists && !updateCheck.exists }, - "A welcome window with the version info off must show neither the app version nor the update check" + waitForPredicate(timeout: 10) { versionLine.exists && updateCheck.exists }, + "Turning the setting back on must restore both" ) } }