1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-08 04:20:07 +02:00

Compare commits

...

8 Commits
v5.8b9 ... v5.8

Author SHA1 Message Date
13ee618d5c 🚀 Version 5.8.0 2023-03-03 16:49:38 +01:00
e149a2500e 📝 Updated README 2023-03-02 16:27:26 +01:00
3f14754177 📝 Update README to reflect #239 2023-02-21 18:18:23 +01:00
b0de0c04c6 🚀 Version 5.7.4 2023-02-14 18:53:32 +01:00
f27e07fc78 🔧 Bump build 2023-02-13 17:30:37 +01:00
9fceab3e2e 🐛 Adjusted for new Homebrew JSON output (#235) 2023-02-13 17:30:01 +01:00
03fdf23f0a 🚀 Version 5.7.3 2023-02-06 19:15:25 +01:00
412b7bad5c 🐛 Fix generated script (#231) 2023-02-06 19:13:49 +01:00
16 changed files with 197 additions and 38 deletions

View File

@ -141,6 +141,7 @@
C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */; };
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; };
C44E985F29B23EBF0059F773 /* UpdateCheckTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44E985E29B23EBF0059F773 /* UpdateCheckTest.swift */; };
C44F868E2835BD8D005C353A /* phpmon-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C44F868D2835BD8D005C353A /* phpmon-config.json */; };
C450C8C628C919EC002A2B4B /* PreferenceName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C450C8C528C919EC002A2B4B /* PreferenceName.swift */; };
C450C8C728C919EC002A2B4B /* PreferenceName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C450C8C528C919EC002A2B4B /* PreferenceName.swift */; };
@ -830,6 +831,7 @@
C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = "<group>"; };
C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableError.swift; sourceTree = "<group>"; };
C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Async.swift"; sourceTree = "<group>"; };
C44E985E29B23EBF0059F773 /* UpdateCheckTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckTest.swift; sourceTree = "<group>"; };
C44F868D2835BD8D005C353A /* phpmon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "phpmon-config.json"; sourceTree = "<group>"; };
C450C8C528C919EC002A2B4B /* PreferenceName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceName.swift; sourceTree = "<group>"; };
C451AFF52969E40F0078E617 /* HelpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpButton.swift; sourceTree = "<group>"; };
@ -1428,6 +1430,7 @@
children = (
C471E7BE28F9B90F0021E251 /* StartupTest.swift */,
C469E702294CFDF700A82AB2 /* DomainsListTest.swift */,
C44E985E29B23EBF0059F773 /* UpdateCheckTest.swift */,
C4181F1028FAF9330042EA28 /* UITestCase.swift */,
);
path = ui;
@ -2504,6 +2507,7 @@
C471E82C28F9BB340021E251 /* ValetListable.swift in Sources */,
C471E82828F9BB310021E251 /* HomebrewDiagnostics.swift in Sources */,
C471E81E28F9BB260021E251 /* BetterAlert.swift in Sources */,
C44E985F29B23EBF0059F773 /* UpdateCheckTest.swift in Sources */,
C471E7D228F9BA630021E251 /* ActiveFileSystem.swift in Sources */,
C471E80028F9BAD10021E251 /* Xdebug.swift in Sources */,
C471E7F528F9BAC80021E251 /* PhpEnv.swift in Sources */,
@ -2885,7 +2889,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1076;
CURRENT_PROJECT_VERSION = 1077;
DEAD_CODE_STRIPPING = YES;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
@ -2915,7 +2919,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1076;
CURRENT_PROJECT_VERSION = 1077;
DEAD_CODE_STRIPPING = YES;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787;
@ -3144,7 +3148,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1076;
CURRENT_PROJECT_VERSION = 1077;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
@ -3255,7 +3259,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1076;
CURRENT_PROJECT_VERSION = 1077;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;

View File

@ -5,15 +5,13 @@
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot.jpg" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
@ -43,13 +41,15 @@ valet install
valet trust
```
#### Manual installation (first time only)
#### Manual installation (recommended, first time only)
Once that's done, you can [download the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest), unzip it and place it in `/Applications`.
#### Installation via Homebrew
If you prefer to install the app via Homebrew, you can also do this:
*Prior to version 5.8, this was the recommended way of installing PHP Monitor.*
If you prefer to install the app via Homebrew, you can also run the following:
```sh
brew tap nicoverbruggen/homebrew-cask
@ -58,9 +58,11 @@ brew install --cask phpmon
## ⬆️ How to update
The recommended method of updating your app to the latest version is to use **the built-in updater**.
The recommended method of updating the app to the latest version is to use **the built-in updater**.
If that doesn't work or you prefer Homebrew, you can also upgrade via those methods.
If you have a very slow internet connection, the updater may report that the download has timed out. In that case, you may wish to manually update by [downloading the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest) and placing the app in `/Applications`.
(You may also use Homebrew to update PHP Monitor, but this will require you to approve the app every time an update is installed. If you use the built-in updater, this won't be necessary.)
## ⚡️ Launchers (Alfred, Raycast)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 627 KiB

View File

@ -13,6 +13,7 @@ public struct TestableConfiguration: Codable {
var filesystem: [String: FakeFile]
var shellOutput: [String: BatchFakeShellOutput]
var commandOutput: [String: String]
var preferenceOverrides: [PreferenceName: Bool]
func apply() {
Log.separator()
@ -31,6 +32,10 @@ public struct TestableConfiguration: Codable {
ServicesManager.useFake()
Log.info("Applying fake Valet domain interactor...")
ValetInteractor.useFake()
Log.info("Applying temporary preference overrides...")
preferenceOverrides.forEach { (key: PreferenceName, value: Any?) in
Preferences.shared.cachedPreferences[key] = value
}
}
func toJson(pretty: Bool = false) -> String {

View File

@ -14,10 +14,10 @@ class AppUpdater {
var latestVersionOnline: AppVersion!
var interactive: Bool = false
public func checkForUpdates(interactive: Bool) async {
self.interactive = interactive
public func checkForUpdates(userInitiated: Bool) async {
self.interactive = userInitiated
if interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Skipping automatic update check due to user preference.")
return
}

View File

@ -39,10 +39,10 @@ struct CaskFile {
}
let lines = string.split(separator: "\n")
.filter { $0 != "" }
.map { line in
return line.trimmingCharacters(in: .whitespacesAndNewlines)
}
.filter { $0 != "" }
if lines.count < 4 {
Log.err("The CaskFile is <4 lines long, which is too short")

View File

@ -111,7 +111,7 @@ extension MainMenu {
OnboardingWindowController.show()
}
} else {
await AppUpdater().checkForUpdates(interactive: false)
await AppUpdater().checkForUpdates(userInitiated: false)
}
}

View File

@ -193,7 +193,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
}
@objc func checkForUpdates() {
Task { await AppUpdater().checkForUpdates(interactive: true) }
Task { await AppUpdater().checkForUpdates(userInitiated: true) }
}
// MARK: - Menu Delegate

View File

@ -48,7 +48,7 @@ class OnboardingWindowController: PMWindowController {
// Search for updates after closing the window
if Stats.successfulLaunchCount == 1 {
Task { await AppUpdater().checkForUpdates(interactive: false) }
Task { await AppUpdater().checkForUpdates(userInitiated: false) }
}
}
}

View File

@ -9,7 +9,7 @@
/**
These are the keys used for every preference in the app.
*/
enum PreferenceName: String {
enum PreferenceName: String, Codable {
// FIRST-TIME LAUNCH
case wasLaunchedBefore = "launched_before"

View File

@ -139,7 +139,21 @@ class TestableConfigurations {
"/opt/homebrew/bin/brew services info --all --json"
: .instant(ShellStrings.shared.brewServicesAsUser),
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
: .instant("version '5.6.2_976'"),
: .delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
version '\(App.shortVersion)_\(App.bundleVersion)'
sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a'
url 'https://github.com/nicoverbruggen/phpmon/releases/download/v\(App.shortVersion)/phpmon-dev.zip'
appcast 'https://github.com/nicoverbruggen/phpmon/releases.atom'
name 'PHP Monitor DEV'
homepage 'https://phpmon.app'
app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app"
end
"""),
"/opt/homebrew/bin/brew unlink php"
: .delayed(0.2, "OK"),
"/opt/homebrew/bin/brew unlink php@8.2"
@ -170,7 +184,8 @@ class TestableConfigurations {
: """
/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini,
"""
]
],
preferenceOverrides: [:]
)
}
}

View File

@ -16,26 +16,14 @@ final class DomainsListTest: UITestCase {
override func tearDownWithError() throws {}
private func openMenu() -> XCPMApplication {
let app = XCPMApplication()
app.withConfiguration(TestableConfigurations.working)
app.launch()
// Note: If this fails here, make sure the menu bar item can be displayed
// If you use Bartender or something like this, this item may be hidden and tests will fail
app.statusItems.firstMatch.click()
return app
}
final func test_can_always_open_domains_list() throws {
let app = openMenu()
let app = launch(openMenu: true)
app.menuItems["mi_domain_list".localized].click()
}
final func test_can_filter_domains_list() throws {
let app = openMenu()
let app = launch(openMenu: true)
app.menuItems["mi_domain_list".localized].click()
@ -58,7 +46,7 @@ final class DomainsListTest: UITestCase {
}
final func test_can_tap_add_domain_button() throws {
let app = openMenu()
let app = launch(openMenu: true)
app.menuItems["mi_domain_list".localized].click()

View File

@ -10,9 +10,33 @@ import XCTest
class UITestCase: XCTestCase {
/** Launches the app and opens the menu. */
public func launch(
openMenu: Bool = false,
with configuration: TestableConfiguration? = nil
) -> XCPMApplication {
let app = XCPMApplication()
let config = configuration ?? TestableConfigurations.working
app.withConfiguration(config)
app.launch()
// Note: If this fails here, make sure the menu bar item can be displayed
// If you use Bartender or something like this, this item may be hidden and tests will fail
if openMenu {
app.statusItems.firstMatch.click()
}
return app
}
/** Checks if a single element exists. */
public func assertExists(_ element: XCUIElement, _ timeout: TimeInterval = 0.05) {
XCTAssert(element.waitForExistence(timeout: timeout))
XCTAssertTrue(element.waitForExistence(timeout: timeout))
}
/** Checks if a single element fails to exist. */
public func assertNotExists(_ element: XCUIElement, _ timeout: TimeInterval = 0.05) {
XCTAssertFalse(element.waitForExistence(timeout: timeout))
}
/** Checks if all elements exist. */

View File

@ -0,0 +1,121 @@
//
// UpdateCheckTest.swift
// UI Tests
//
// Created by Nico Verbruggen on 13/03/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
final class UpdateCheckTest: UITestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {}
final func test_can_check_for_updates_with_no_new_update() throws {
let app = launch(openMenu: true)
app.menuItems["mi_check_for_updates".localized].click()
assertExists(app.staticTexts["updater.alerts.is_latest_version.title".localized], 1.0)
assertExists(app.buttons["generic.ok".localized])
}
final func test_will_prompt_at_launch_new_version_available() throws {
var configuration = TestableConfigurations.working
// Ensure automatic check is enabled
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = true
// Ensure an update is available
configuration.shellOutput[
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
] = .delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
version '99.0.0_9999'
sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a'
url 'https://github.com/nicoverbruggen/phpmon/releases/download/v99.0/phpmon-dev.zip'
appcast 'https://github.com/nicoverbruggen/phpmon/releases.atom'
name 'PHP Monitor DEV'
homepage 'https://phpmon.app'
app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app"
end
""")
let app = launch(openMenu: false, with: configuration)
// Expect to see the content of the appropriate alert box
assertExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], 2)
assertExists(app.buttons["updater.alerts.buttons.install".localized])
assertExists(app.buttons["updater.alerts.buttons.dismiss".localized])
}
final func test_will_require_manual_search_for_update() throws {
var configuration = TestableConfigurations.working
// Ensure automatic check is disabled
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false
// Ensure an update is available
configuration.shellOutput[
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
] = .delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
version '99.0.0_9999'
sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a'
url 'https://github.com/nicoverbruggen/phpmon/releases/download/v99.0/phpmon-dev.zip'
appcast 'https://github.com/nicoverbruggen/phpmon/releases.atom'
name 'PHP Monitor DEV'
homepage 'https://phpmon.app'
app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app"
end
""")
// Wait for the menu to open and search for updates
let app = launch(openMenu: false, with: configuration)
// The check should not happen if the preference is disabled
assertNotExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], 2)
// Open the menu and check manually
app.statusItems.firstMatch.click()
app.menuItems["mi_check_for_updates".localized].click()
// Expect to see the content of the appropriate alert box
assertExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], 2)
assertExists(app.buttons["updater.alerts.buttons.install".localized])
assertExists(app.buttons["updater.alerts.buttons.dismiss".localized])
}
final func test_could_not_parse_version() throws {
var configuration = TestableConfigurations.working
// Ensure automatic check is disabled
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false
// Ensure an update is available
configuration.shellOutput[
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
] = .delayed(0.5, "404 PAGE NOT FOUND")
// Wait for the menu to open and search for updates
let app = launch(openMenu: true, with: configuration)
app.menuItems["mi_check_for_updates".localized].click()
// Expect to see the content of the appropriate alert box
assertExists(app.staticTexts["updater.alerts.cannot_check_for_update.title".localized], 2)
assertExists(app.buttons["generic.ok".localized])
assertExists(app.buttons["updater.alerts.buttons.releases_on_github".localized])
}
}