TrayTempo v0.2.0 : minuteur Pomodoro Windows en Nim avec interface intuitive
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
## test_config.nim
|
||||
## ================
|
||||
## Tests d'intégration de config.nim : valeurs par défaut, résolution de profil,
|
||||
## round-trip JSON (save/load), tolérance aux fichiers corrompus, et
|
||||
## démarrage automatique via la clé de registre Run.
|
||||
|
||||
import std/[unittest, os]
|
||||
import ../src/config
|
||||
|
||||
# Redirige APPDATA vers un répertoire temporaire pour isoler les fichiers.
|
||||
let tmpApd = getTempDir() / "traytempo_test_apd"
|
||||
if dirExists(tmpApd):
|
||||
removeDir(tmpApd)
|
||||
putEnv("APPDATA", tmpApd)
|
||||
|
||||
suite "config":
|
||||
test "defaultConfig has expected values":
|
||||
let c = defaultConfig()
|
||||
check c.workMin == 25
|
||||
check c.shortBreakMin == 5
|
||||
check c.longBreakMin == 15
|
||||
check c.cyclesBeforeLongBreak == 4
|
||||
check c.soundEnabled == true
|
||||
check c.doNotDisturb == false
|
||||
check c.autoStart == false
|
||||
check c.activeProfile == "Classique"
|
||||
check c.floatingWindow == false
|
||||
check c.profiles.len == 3
|
||||
check c.healthReminders.len == 5
|
||||
|
||||
test "getActiveProfile returns the matching profile":
|
||||
let c = defaultConfig()
|
||||
let p = c.getActiveProfile()
|
||||
check p.name == "Classique"
|
||||
check p.workMin == 25
|
||||
check p.cycles == 4
|
||||
|
||||
test "getActiveProfile falls back to defaults for an unknown name":
|
||||
var c = defaultConfig()
|
||||
c.activeProfile = "N'existe pas"
|
||||
let p = c.getActiveProfile()
|
||||
check p.name == "Défaut"
|
||||
check p.workMin == c.workMin
|
||||
|
||||
test "saveConfig then loadConfig round-trips (including accents)":
|
||||
var c = defaultConfig()
|
||||
c.workMin = 42
|
||||
c.doNotDisturb = true
|
||||
c.activeProfile = "Sprint"
|
||||
saveConfig(c)
|
||||
let l = loadConfig()
|
||||
check l.workMin == 42
|
||||
check l.doNotDisturb == true
|
||||
check l.activeProfile == "Sprint"
|
||||
check l.profiles.len == 3
|
||||
check l.healthReminders.len == 5
|
||||
# UTF-8 (accents) doit survivre à l'aller-retour JSON.
|
||||
check l.healthReminders[0].text == "Lève-toi et marche 2 minutes"
|
||||
|
||||
test "loadConfig returns defaults on a corrupt file":
|
||||
ensureConfigDir()
|
||||
writeFile(configPath(), "{ pas du json valide")
|
||||
let l = loadConfig()
|
||||
check l.workMin == 25
|
||||
check l.profiles.len == 3
|
||||
|
||||
test "new fields (lang, dailyGoal, hotkey, sound) round-trip":
|
||||
var c = defaultConfig()
|
||||
c.lang = "en"
|
||||
c.dailyGoal = 8
|
||||
c.hotkeyModifiers = 0x0003'i32
|
||||
c.hotkeyVk = 0x50'i32
|
||||
c.soundName = "SystemExclamation"
|
||||
c.floatingWindow = true
|
||||
saveConfig(c)
|
||||
let l = loadConfig()
|
||||
check l.lang == "en"
|
||||
check l.dailyGoal == 8
|
||||
check l.hotkeyModifiers == 0x0003'i32
|
||||
check l.hotkeyVk == 0x50'i32
|
||||
check l.soundName == "SystemExclamation"
|
||||
check l.floatingWindow == true
|
||||
|
||||
test "English defaults localize profiles and reminders":
|
||||
let c = defaultConfig(langEn)
|
||||
check c.activeProfile == "Classic"
|
||||
check c.profiles[0].name == "Classic"
|
||||
check c.healthReminders[0].text == "Stand up and walk for 2 minutes"
|
||||
|
||||
test "loadConfig keeps defaults for missing new fields":
|
||||
# Un ancien fichier sans les nouveaux champs doit rester lisible.
|
||||
ensureConfigDir()
|
||||
writeFile(configPath(), "{\"workMin\": 30}")
|
||||
let l = loadConfig()
|
||||
check l.workMin == 30
|
||||
check l.lang == "auto"
|
||||
check l.dailyGoal == 0
|
||||
|
||||
test "configDir returns exe dir in portable mode":
|
||||
let exeDir = getAppFilename().parentDir
|
||||
let marker = exeDir / "portable"
|
||||
writeFile(marker, "")
|
||||
check configDir() == exeDir
|
||||
removeFile(marker)
|
||||
check configDir() == (getEnv("APPDATA") / "TrayTempo")
|
||||
|
||||
test "acquireSingleInstance detects a second lock":
|
||||
check not acquireSingleInstance("TrayTempo_Test_Mutex_0.2.0")
|
||||
check acquireSingleInstance("TrayTempo_Test_Mutex_0.2.0")
|
||||
|
||||
suite "autostart registry":
|
||||
test "setAutostart toggles the Run key and cleans up":
|
||||
let before = isAutostartEnabled()
|
||||
check setAutostart(true)
|
||||
check isAutostartEnabled()
|
||||
check setAutostart(false)
|
||||
check not isAutostartEnabled()
|
||||
# Restaure l'état d'origine pour ne rien laisser traîner.
|
||||
if before:
|
||||
discard setAutostart(true)
|
||||
@@ -0,0 +1,34 @@
|
||||
## test_dialogs.nim
|
||||
## =================
|
||||
## Test d'intégration de la construction du panneau de réglages : vérifie que la
|
||||
## fenêtre et ses contrôles sont réellement créés (sans lancer la boucle modale,
|
||||
## qui bloquerait). Cela rend observable la création de l'interface, au-delà de
|
||||
## la simple compilation.
|
||||
|
||||
import std/unittest
|
||||
import winim/lean
|
||||
import ../src/dialogs
|
||||
import ../src/config
|
||||
|
||||
proc createParent(): HWND =
|
||||
let hInstance = GetModuleHandleW(nil)
|
||||
var wc: WNDCLASSW
|
||||
wc.lpfnWndProc = cast[WNDPROC](DefWindowProcW)
|
||||
wc.hInstance = hInstance
|
||||
wc.lpszClassName = L"TrayTempoDialogTestClass"
|
||||
discard RegisterClass(&wc)
|
||||
result = CreateWindowExW(0, L"TrayTempoDialogTestClass", L"", WS_OVERLAPPED,
|
||||
0, 0, 0, 0, 0, 0, hInstance, nil)
|
||||
|
||||
suite "dialogs":
|
||||
test "settings dialog window and controls are built":
|
||||
let parent = createParent()
|
||||
check parent != 0
|
||||
|
||||
let hwnd = buildSettingsDialog(parent, defaultConfig())
|
||||
check hwnd != 0
|
||||
# La fenêtre doit avoir au moins un contrôle enfant (champ, bouton, etc.).
|
||||
check GetWindow(hwnd, GW_CHILD) != 0
|
||||
|
||||
discard DestroyWindow(hwnd)
|
||||
discard DestroyWindow(parent)
|
||||
@@ -0,0 +1,18 @@
|
||||
## test_floating.nim
|
||||
## ==================
|
||||
## Test d'intégration de la fenêtre flottante : crée, met à jour puis détruit
|
||||
## la vraie fenêtre Win32 et vérifie que l'état est cohérent. Ce test réduit le
|
||||
## fossé entre « compile » et « observé » pour cette fonctionnalité.
|
||||
|
||||
import std/unittest
|
||||
import ../src/floating
|
||||
|
||||
suite "floating window":
|
||||
test "show, update and hide manage a real window":
|
||||
check not floatingVisible()
|
||||
showFloating()
|
||||
check floatingVisible()
|
||||
# La mise à jour du texte ne doit pas lever d'erreur.
|
||||
updateFloating("Travail 24:59")
|
||||
hideFloating()
|
||||
check not floatingVisible()
|
||||
@@ -0,0 +1,43 @@
|
||||
## test_notifications.nim
|
||||
## ======================
|
||||
## Tests unitaires de la rotation des rappels santé (logique pure). Aucun
|
||||
## appel Win32 : on ne teste que nextHealthReminder.
|
||||
|
||||
import std/unittest
|
||||
import ../src/notifications
|
||||
import ../src/config
|
||||
|
||||
suite "health reminder rotation":
|
||||
test "does not repeat the same reminder twice in a row":
|
||||
let cfg = defaultConfig()
|
||||
let a = nextHealthReminder(cfg)
|
||||
let b = nextHealthReminder(cfg)
|
||||
check a.len > 0
|
||||
check b.len > 0
|
||||
check a != b
|
||||
|
||||
test "a single enabled reminder is always returned":
|
||||
var cfg = defaultConfig()
|
||||
for i in 0 ..< cfg.healthReminders.len:
|
||||
cfg.healthReminders[i].enabled = (i == 0)
|
||||
check nextHealthReminder(cfg) == "Lève-toi et marche 2 minutes"
|
||||
check nextHealthReminder(cfg) == "Lève-toi et marche 2 minutes"
|
||||
|
||||
test "returns empty string when nothing is enabled":
|
||||
var cfg = defaultConfig()
|
||||
for i in 0 ..< cfg.healthReminders.len:
|
||||
cfg.healthReminders[i].enabled = false
|
||||
check nextHealthReminder(cfg) == ""
|
||||
|
||||
test "skips disabled reminders":
|
||||
var cfg = defaultConfig()
|
||||
for i in 0 ..< cfg.healthReminders.len:
|
||||
cfg.healthReminders[i].enabled = (i == 2)
|
||||
check nextHealthReminder(cfg) == cfg.healthReminders[2].text
|
||||
|
||||
test "custom reminder text is returned verbatim":
|
||||
var cfg = defaultConfig()
|
||||
for i in 0 ..< cfg.healthReminders.len:
|
||||
cfg.healthReminders[i].enabled = false
|
||||
cfg.healthReminders.add HealthReminder(text: "Médite 5 minutes", enabled: true)
|
||||
check nextHealthReminder(cfg) == "Médite 5 minutes"
|
||||
@@ -0,0 +1,106 @@
|
||||
## test_stats.nim
|
||||
## ==============
|
||||
## Tests unitaires des statistiques (compteurs jour/semaine et streak).
|
||||
## Aucune écriture disque : on manipule directement la table `daily`.
|
||||
|
||||
import std/unittest
|
||||
import std/times
|
||||
import std/tables
|
||||
import std/os
|
||||
import std/strutils
|
||||
import ../src/stats
|
||||
|
||||
suite "stats":
|
||||
test "initStats starts empty":
|
||||
let st = initStats()
|
||||
check st.completedToday() == 0
|
||||
check st.completedThisWeek() == 0
|
||||
check st.streak() == 0
|
||||
|
||||
test "completedToday counts today's pomodoros":
|
||||
var st = initStats()
|
||||
st.daily[todayStr()] = 5
|
||||
check st.completedToday() == 5
|
||||
|
||||
test "streak counts consecutive days ending today":
|
||||
var st = initStats()
|
||||
let today = now()
|
||||
for i in 0 ..< 3:
|
||||
st.daily[(today - i.days).format("yyyy-MM-dd")] = 1
|
||||
check st.streak() == 3
|
||||
|
||||
test "streak starts from yesterday when today has no entry":
|
||||
var st = initStats()
|
||||
let today = now()
|
||||
st.daily[(today - 1.days).format("yyyy-MM-dd")] = 1
|
||||
st.daily[(today - 2.days).format("yyyy-MM-dd")] = 1
|
||||
check st.streak() == 2
|
||||
|
||||
test "streak stops at the first gap":
|
||||
var st = initStats()
|
||||
let today = now()
|
||||
st.daily[today.format("yyyy-MM-dd")] = 1
|
||||
st.daily[(today - 1.days).format("yyyy-MM-dd")] = 1
|
||||
# jour -2 manquant (gap)
|
||||
st.daily[(today - 3.days).format("yyyy-MM-dd")] = 1
|
||||
check st.streak() == 2
|
||||
|
||||
test "completedThisWeek sums the last 7 days":
|
||||
var st = initStats()
|
||||
let today = now()
|
||||
for i in 0 ..< 7:
|
||||
st.daily[(today - i.days).format("yyyy-MM-dd")] = 1
|
||||
check st.completedThisWeek() == 7
|
||||
|
||||
test "totalPomodoros sums the whole history":
|
||||
var st = initStats()
|
||||
st.daily["2026-01-01"] = 2
|
||||
st.daily["2026-01-02"] = 3
|
||||
st.daily["2026-01-03"] = 5
|
||||
check st.totalPomodoros() == 10
|
||||
|
||||
test "goalReached is true only when the goal is exactly met":
|
||||
var st = initStats()
|
||||
st.daily[todayStr()] = 8
|
||||
check st.goalReached(8)
|
||||
check not st.goalReached(4)
|
||||
check not st.goalReached(0) # objectif 0 = désactivé
|
||||
|
||||
test "lastNDays returns n entries including empty days":
|
||||
var st = initStats()
|
||||
let today = now()
|
||||
st.daily[today.format("yyyy-MM-dd")] = 4
|
||||
let rows = st.lastNDays(7)
|
||||
check rows.len == 7
|
||||
# Le dernier élément correspond à aujourd'hui.
|
||||
check rows[^1][0] == today.format("yyyy-MM-dd")
|
||||
check rows[^1][1] == 4
|
||||
|
||||
test "exportCsv writes date,count lines":
|
||||
var st = initStats()
|
||||
st.daily["2026-08-13"] = 3
|
||||
st.daily["2026-08-14"] = 5
|
||||
let path = getTempDir() / "traytempo_test_export.csv"
|
||||
check st.exportCsv(path)
|
||||
let content = readFile(path)
|
||||
check content.contains("date,count")
|
||||
check content.contains("2026-08-13,3")
|
||||
check content.contains("2026-08-14,5")
|
||||
removeFile(path)
|
||||
|
||||
suite "stats persistence":
|
||||
test "saveStats then loadStats round-trips":
|
||||
# Isole l'écriture disque dans un APPDATA temporaire.
|
||||
let tmpApd = getTempDir() / "traytempo_test_stats"
|
||||
if dirExists(tmpApd):
|
||||
removeDir(tmpApd)
|
||||
putEnv("APPDATA", tmpApd)
|
||||
|
||||
var st = initStats()
|
||||
st.daily["2026-08-13"] = 3
|
||||
st.daily["2026-08-14"] = 5
|
||||
saveStats(st)
|
||||
|
||||
let loaded = loadStats()
|
||||
check loaded.daily["2026-08-13"] == 3
|
||||
check loaded.daily["2026-08-14"] == 5
|
||||
@@ -0,0 +1,144 @@
|
||||
## test_timer.nim
|
||||
## ==============
|
||||
## Tests unitaires de la machine à états du minuteur (module pur, sans Win32).
|
||||
|
||||
import std/unittest
|
||||
import ../src/timer
|
||||
import ../src/config
|
||||
|
||||
proc testProfile(): Profile =
|
||||
Profile(name: "Test", workMin: 25, shortBreakMin: 5, longBreakMin: 15, cycles: 4)
|
||||
|
||||
suite "timer":
|
||||
test "initial state is idle":
|
||||
let t = initTimer()
|
||||
check t.phase == phIdle
|
||||
check t.remainingSecs == 0
|
||||
check t.cycleCount == 0
|
||||
|
||||
test "startWork sets working phase and duration":
|
||||
var t = initTimer()
|
||||
t.startWork(testProfile())
|
||||
check t.phase == phWorking
|
||||
check t.remainingSecs == 25 * 60
|
||||
|
||||
test "tick decrements remaining by one second":
|
||||
var t = initTimer()
|
||||
t.startWork(testProfile())
|
||||
let ev = t.tick(testProfile())
|
||||
check ev == evNone
|
||||
check t.remainingSecs == 25 * 60 - 1
|
||||
|
||||
test "tick does nothing when idle":
|
||||
var t = initTimer()
|
||||
check t.tick(testProfile()) == evNone
|
||||
check t.phase == phIdle
|
||||
|
||||
test "work completion transitions to short break (1 cycle)":
|
||||
var t = initTimer()
|
||||
let p = Profile(name: "T", workMin: 1, shortBreakMin: 5, longBreakMin: 15, cycles: 4)
|
||||
t.startWork(p)
|
||||
var ev = evNone
|
||||
for i in 0 ..< 60:
|
||||
ev = t.tick(p)
|
||||
check ev == evWorkCompleted
|
||||
check t.phase == phShortBreak
|
||||
check t.remainingSecs == 5 * 60
|
||||
check t.cycleCount == 1
|
||||
|
||||
test "break completion transitions back to work":
|
||||
var t = initTimer()
|
||||
let p = Profile(name: "T", workMin: 25, shortBreakMin: 1, longBreakMin: 15, cycles: 4)
|
||||
t.startBreak(p, longBreak = false)
|
||||
var ev = evNone
|
||||
for i in 0 ..< 60:
|
||||
ev = t.tick(p)
|
||||
check ev == evBreakCompleted
|
||||
check t.phase == phWorking
|
||||
check t.remainingSecs == 25 * 60
|
||||
|
||||
test "cyclesBeforeLongBreak triggers a long break":
|
||||
var t = initTimer()
|
||||
let p = Profile(name: "T", workMin: 1, shortBreakMin: 5, longBreakMin: 15, cycles: 4)
|
||||
t.cycleCount = 3 # la 4e période de travail doit déclencher la pause longue
|
||||
t.startWork(p)
|
||||
var ev = evNone
|
||||
for i in 0 ..< 60:
|
||||
ev = t.tick(p)
|
||||
check ev == evWorkCompleted
|
||||
check t.phase == phLongBreak
|
||||
check t.remainingSecs == 15 * 60
|
||||
|
||||
test "pause preserves the remaining time and resume restores phase":
|
||||
var t = initTimer()
|
||||
t.startWork(testProfile())
|
||||
let before = t.remainingSecs
|
||||
t.pause()
|
||||
check t.phase == phPaused
|
||||
check t.remainingSecs == before
|
||||
t.resume()
|
||||
check t.phase == phWorking
|
||||
|
||||
test "pause on idle does nothing":
|
||||
var t = initTimer()
|
||||
t.pause()
|
||||
check t.phase == phIdle
|
||||
|
||||
test "stop returns to idle":
|
||||
var t = initTimer()
|
||||
t.startWork(testProfile())
|
||||
t.stop()
|
||||
check t.phase == phIdle
|
||||
check t.remainingSecs == 0
|
||||
|
||||
test "stop resets cycleCount so long break timing stays correct":
|
||||
var t = initTimer()
|
||||
let p = Profile(name: "T", workMin: 1, shortBreakMin: 1, longBreakMin: 1, cycles: 4)
|
||||
t.cycleCount = 3
|
||||
t.stop()
|
||||
check t.cycleCount == 0
|
||||
# Après redémarrage, il faut 4 cycles (pas 1) avant la pause longue.
|
||||
t.startWork(p)
|
||||
check t.cycleCount == 0
|
||||
|
||||
test "isBreak is true only during breaks":
|
||||
var t = initTimer()
|
||||
check not t.isBreak()
|
||||
t.startWork(testProfile())
|
||||
check not t.isBreak()
|
||||
t.startBreak(testProfile(), longBreak = false)
|
||||
check t.isBreak()
|
||||
t.startBreak(testProfile(), longBreak = true)
|
||||
check t.isBreak()
|
||||
|
||||
test "skipBreak jumps from break to work":
|
||||
var t = initTimer()
|
||||
let p = testProfile()
|
||||
t.startBreak(p, longBreak = false)
|
||||
t.skipBreak(p)
|
||||
check t.phase == phWorking
|
||||
check t.remainingSecs == p.workMin * 60
|
||||
|
||||
test "skipBreak does nothing while working":
|
||||
var t = initTimer()
|
||||
t.startWork(testProfile())
|
||||
t.skipBreak(testProfile())
|
||||
check t.phase == phWorking
|
||||
|
||||
test "extendBreak adds minutes to a break":
|
||||
var t = initTimer()
|
||||
t.startBreak(testProfile(), longBreak = false)
|
||||
let before = t.remainingSecs
|
||||
t.extendBreak(5)
|
||||
check t.remainingSecs == before + 5 * 60
|
||||
|
||||
test "extendBreak does nothing while working":
|
||||
var t = initTimer()
|
||||
t.startWork(testProfile())
|
||||
t.extendBreak(5)
|
||||
check t.phase == phWorking
|
||||
|
||||
test "formatTime formats minutes and seconds":
|
||||
check formatTime(0) == "0:00"
|
||||
check formatTime(65) == "1:05"
|
||||
check formatTime(3661) == "1:01:01"
|
||||
@@ -0,0 +1,55 @@
|
||||
## test_tray_integration.nim
|
||||
## ==========================
|
||||
## Test d'intégration Win32 : crée une vraie fenêtre cachée, puis exerce les
|
||||
## interfaces publiques de `tray.nim` et `notifications.nim` contre les API
|
||||
## réelles (user32/shell32) en vérifiant leurs codes de retour.
|
||||
##
|
||||
## Note : ce test fait brièvement apparaître une icône tray et une notification
|
||||
## sur le bureau, puis nettoie tout à la fin.
|
||||
|
||||
import std/unittest
|
||||
import winim/lean
|
||||
|
||||
import ../src/tray
|
||||
import ../src/notifications
|
||||
import ../src/timer
|
||||
|
||||
proc createTestWindow(): HWND =
|
||||
let hInstance = GetModuleHandleW(nil)
|
||||
var wc: WNDCLASSW
|
||||
wc.lpfnWndProc = cast[WNDPROC](DefWindowProcW)
|
||||
wc.hInstance = hInstance
|
||||
wc.lpszClassName = L"TrayTempoTestClass"
|
||||
discard RegisterClass(&wc)
|
||||
result = CreateWindowExW(0, L"TrayTempoTestClass", L"", WS_OVERLAPPED,
|
||||
0, 0, 0, 0, 0, 0, hInstance, nil)
|
||||
|
||||
suite "tray integration":
|
||||
test "tray icon, dynamic icon, toast and hotkey succeed":
|
||||
let hwnd = createTestWindow()
|
||||
check hwnd != 0
|
||||
|
||||
# Génération des icônes dynamiques (GDI) pour les 5 états.
|
||||
for phase in [phIdle, phWorking, phShortBreak, phLongBreak, phPaused]:
|
||||
check phaseIcon(phase) != 0
|
||||
|
||||
# Ajout de l'icône dans le tray (Shell_NotifyIcon NIM_ADD -> bool).
|
||||
check addTrayIcon(hwnd, phaseIcon(phIdle), "TrayTempo") == true
|
||||
|
||||
# Mise à jour de l'icône et du tooltip.
|
||||
updateTrayIcon(hwnd, phaseIcon(phWorking), "Travail: 25:00")
|
||||
|
||||
# Notification (NIF_INFO).
|
||||
showToast(hwnd, "Test", "Notification d'intégration")
|
||||
|
||||
# Raccourci global (RegisterHotKey / UnregisterHotKey). Clé de test peu
|
||||
# susceptible d'être déjà réservée (Ctrl+Alt+F10).
|
||||
check RegisterHotKey(hwnd, 1'i32, MOD_CONTROL or MOD_ALT, 0x79) != 0
|
||||
check UnregisterHotKey(hwnd, 1'i32) != 0
|
||||
|
||||
# Son de fin de période (alias système valide -> PlaySound retourne true).
|
||||
check playSound("SystemAsterisk") == true
|
||||
|
||||
# Suppression de l'icône et de la fenêtre.
|
||||
removeTrayIcon(hwnd)
|
||||
discard DestroyWindow(hwnd)
|
||||
@@ -0,0 +1,29 @@
|
||||
## test_updater.nim
|
||||
## ==================
|
||||
## Tests unitaires des fonctions pures du module updater (comparaison de
|
||||
## versions). Aucun accès réseau.
|
||||
|
||||
import std/unittest
|
||||
import ../src/updater
|
||||
|
||||
suite "updater":
|
||||
test "parseVersion splits dotted numbers":
|
||||
check parseVersion("0.2.0") == @[0, 2, 0]
|
||||
check parseVersion("1.10.3") == @[1, 10, 3]
|
||||
check parseVersion("2") == @[2]
|
||||
|
||||
test "parseVersion tolerates non-numeric components":
|
||||
check parseVersion("v1.2") == @[0, 2]
|
||||
|
||||
test "isNewer detects a newer version":
|
||||
check isNewer("0.2.0", "0.3.0")
|
||||
check isNewer("0.2.9", "0.2.10")
|
||||
check isNewer("1.0.0", "1.0.1")
|
||||
|
||||
test "isNewer returns false for equal or older versions":
|
||||
check not isNewer("0.3.0", "0.3.0")
|
||||
check not isNewer("0.3.0", "0.2.9")
|
||||
check not isNewer("1.0.0", "0.9.9")
|
||||
|
||||
test "fetchLatestVersion returns empty for empty URL":
|
||||
check fetchLatestVersion("") == ""
|
||||
Reference in New Issue
Block a user