Seconds move from one bar-wide switch to a per-clock setting. A clock's own `showSeconds` wins, the top-level one is the default for clocks with no opinion, and the centre clock gets `local.showSeconds` since it has no entry in `clocks[]`. An explicit false on a clock beats a true default, so the check is for the property's presence rather than its truthiness. The tick rate follows what is actually displayed: `anyShowsSeconds()` decides between a per-second and a per-minute SystemClock, so a bar showing no seconds does not wake the shell sixty times as often to redraw an identical minute. `barSize` is now editable from the popup, and everything drawn on the bar is sized against the theme's own bar height rather than pinned to the theme's 12px type. A taller bar carries proportionally larger text, gaps and rule; at the theme's own height the scale is exactly 1, so nothing moves. Heights are clamped to 12-120 rather than rejected, so a value the editor can produce can never be one the config layer silently discards. Fixes a latent bug this surfaced: updateRow() rebuilt a draft row from a fixed pair of fields, so editing a clock's label would have thrown away its own seconds setting. It now copies every key the row had. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MhxtL7WzL5XmuTd8GHbcTY
253 lines
10 KiB
JavaScript
253 lines
10 KiB
JavaScript
// Unit tests for ClockModel.js. Run with: node tests/model.test.js
|
|
// No dependencies — the model is plain ES5 so it runs identically here and in
|
|
// Qt's QML engine.
|
|
|
|
var assert = require("assert")
|
|
var M = require("../ClockModel.js")
|
|
|
|
var passed = 0
|
|
function test(name, fn) {
|
|
try { fn(); passed++ }
|
|
catch (e) { console.error("FAIL: " + name + "\n " + e.message); process.exitCode = 1 }
|
|
}
|
|
|
|
// 2026-09-04T15:24:00Z — a Friday. Johannesburg is +0200, so local is 17:24.
|
|
var NOW = Date.UTC(2026, 8, 4, 15, 24, 0)
|
|
var SAST = "+0200"
|
|
|
|
var OFFSETS = {
|
|
"America/Los_Angeles": "-0700", // PDT
|
|
"America/New_York": "-0400", // EDT
|
|
"Asia/Singapore": "+0800",
|
|
"Australia/Sydney": "+1000", // AEST
|
|
"Pacific/Auckland": "+1200" // NZST
|
|
}
|
|
|
|
var CONFIG = {
|
|
clocks: [
|
|
{ label: "NZ", timezone: "Pacific/Auckland" },
|
|
{ label: "LA", timezone: "America/Los_Angeles" },
|
|
{ label: "AUS", timezone: "Australia/Sydney" },
|
|
{ label: "NYC", timezone: "America/New_York" },
|
|
{ label: "SG", timezone: "Asia/Singapore" }
|
|
],
|
|
workingHours: { start: 8, end: 18, workdays: [1, 2, 3, 4, 5] }
|
|
}
|
|
|
|
test("parseOffset handles the tzdata forms", function () {
|
|
assert.strictEqual(M.parseOffset("+0200"), 120)
|
|
assert.strictEqual(M.parseOffset("-0700"), -420)
|
|
assert.strictEqual(M.parseOffset("+0545"), 345) // Kathmandu
|
|
assert.strictEqual(M.parseOffset("-0330"), -210) // Newfoundland
|
|
assert.strictEqual(M.parseOffset("+00:00"), 0)
|
|
assert.strictEqual(M.parseOffset(""), null)
|
|
assert.strictEqual(M.parseOffset("garbage"), null)
|
|
assert.strictEqual(M.parseOffset(null), null)
|
|
})
|
|
|
|
test("times are correct against known offsets", function () {
|
|
var bar = M.buildBar(CONFIG, OFFSETS, NOW, SAST)
|
|
var byLabel = {}
|
|
bar.left.concat(bar.right).forEach(function (c) { byLabel[c.label] = c })
|
|
|
|
assert.strictEqual(bar.center.time, "17:24", "local")
|
|
assert.strictEqual(byLabel.LA.time, "08:24")
|
|
assert.strictEqual(byLabel.NYC.time, "11:24")
|
|
assert.strictEqual(byLabel.SG.time, "23:24")
|
|
assert.strictEqual(byLabel.AUS.time, "01:24")
|
|
assert.strictEqual(byLabel.NZ.time, "03:24")
|
|
})
|
|
|
|
test("clocks land on the correct side of local", function () {
|
|
var bar = M.buildBar(CONFIG, OFFSETS, NOW, SAST)
|
|
assert.deepStrictEqual(bar.left.map(function (c) { return c.label }), ["LA", "NYC"])
|
|
assert.deepStrictEqual(bar.right.map(function (c) { return c.label }), ["SG", "AUS", "NZ"])
|
|
})
|
|
|
|
test("config order does not affect rendered order", function () {
|
|
var shuffled = { clocks: CONFIG.clocks.slice().reverse(), workingHours: CONFIG.workingHours }
|
|
var a = M.buildBar(CONFIG, OFFSETS, NOW, SAST)
|
|
var b = M.buildBar(shuffled, OFFSETS, NOW, SAST)
|
|
var names = function (bar) { return bar.left.concat(bar.right).map(function (c) { return c.label }) }
|
|
assert.deepStrictEqual(names(a), names(b))
|
|
})
|
|
|
|
test("day markers show +1 and -1", function () {
|
|
var bar = M.buildBar(CONFIG, OFFSETS, NOW, SAST)
|
|
var byLabel = {}
|
|
bar.left.concat(bar.right).forEach(function (c) { byLabel[c.label] = c })
|
|
assert.strictEqual(byLabel.AUS.marker, "+1", "Sydney is already tomorrow")
|
|
assert.strictEqual(byLabel.NZ.marker, "+1")
|
|
assert.strictEqual(byLabel.SG.marker, "", "Singapore is still today")
|
|
assert.strictEqual(byLabel.LA.marker, "", "LA is still today at 08:24")
|
|
|
|
// Same zones, but local is 01:24 on the 5th — LA is then on the 4th.
|
|
var early = Date.UTC(2026, 8, 4, 23, 24, 0)
|
|
var lateBar = M.buildBar(CONFIG, OFFSETS, early, SAST)
|
|
var late = {}
|
|
lateBar.left.concat(lateBar.right).forEach(function (c) { late[c.label] = c })
|
|
assert.strictEqual(lateBar.center.time, "01:24")
|
|
assert.strictEqual(late.LA.marker, "-1", "LA is yesterday")
|
|
assert.strictEqual(late.NYC.marker, "-1")
|
|
})
|
|
|
|
test("working hours are evaluated in the clock's own zone", function () {
|
|
var bar = M.buildBar(CONFIG, OFFSETS, NOW, SAST)
|
|
var byLabel = {}
|
|
bar.left.concat(bar.right).forEach(function (c) { byLabel[c.label] = c })
|
|
assert.strictEqual(byLabel.LA.working, true, "08:24 Fri in LA")
|
|
assert.strictEqual(byLabel.NYC.working, true, "11:24 Fri in NYC")
|
|
assert.strictEqual(byLabel.SG.working, false, "23:24 Fri in Singapore")
|
|
assert.strictEqual(byLabel.NZ.working, false, "03:24 Sat in Auckland")
|
|
})
|
|
|
|
test("weekends count as outside working hours", function () {
|
|
// 2026-09-05 is a Saturday. 10:00 local.
|
|
var sat = Date.UTC(2026, 8, 5, 8, 0, 0)
|
|
var bar = M.buildBar({ clocks: [{ label: "JHB", timezone: "Africa/Johannesburg" }] },
|
|
{ "Africa/Johannesburg": "+0200" }, sat, SAST)
|
|
assert.strictEqual(bar.right[0].time, "10:00")
|
|
assert.strictEqual(bar.right[0].working, false)
|
|
})
|
|
|
|
test("a window that wraps midnight is handled", function () {
|
|
var hours = { start: 22, end: 6, workdays: [0, 1, 2, 3, 4, 5, 6] }
|
|
assert.strictEqual(M.isWorkingHours(new Date(Date.UTC(2026, 8, 4, 23, 0)), hours), true)
|
|
assert.strictEqual(M.isWorkingHours(new Date(Date.UTC(2026, 8, 4, 3, 0)), hours), true)
|
|
assert.strictEqual(M.isWorkingHours(new Date(Date.UTC(2026, 8, 4, 12, 0)), hours), false)
|
|
})
|
|
|
|
test("an unresolvable zone is flagged, not rendered as UTC", function () {
|
|
var bar = M.buildBar({ clocks: [{ label: "BAD", timezone: "Not/AZone" }] }, {}, NOW, SAST)
|
|
var clock = bar.right[0]
|
|
assert.strictEqual(clock.known, false)
|
|
assert.strictEqual(clock.time, "--:--")
|
|
assert.strictEqual(bar.left.length, 0, "unknown zones never sort into the timeline")
|
|
})
|
|
|
|
test("half-hour and quarter-hour zones sort correctly", function () {
|
|
var cfg = {
|
|
clocks: [
|
|
{ label: "IND", timezone: "Asia/Kolkata" },
|
|
{ label: "NPL", timezone: "Asia/Kathmandu" },
|
|
{ label: "SG", timezone: "Asia/Singapore" }
|
|
]
|
|
}
|
|
var offs = { "Asia/Kolkata": "+0530", "Asia/Kathmandu": "+0545", "Asia/Singapore": "+0800" }
|
|
var bar = M.buildBar(cfg, offs, NOW, SAST)
|
|
assert.deepStrictEqual(bar.right.map(function (c) { return c.label }), ["IND", "NPL", "SG"])
|
|
assert.strictEqual(bar.right[0].time, "20:54")
|
|
assert.strictEqual(bar.right[1].time, "21:09")
|
|
})
|
|
|
|
test("a DST shift re-sorts the bar with no config change", function () {
|
|
// Sydney +1000 sits behind Auckland +1200; in the southern summer both move
|
|
// but keep order, while LA leaving PDT widens its gap from local.
|
|
var winter = M.buildBar(CONFIG, OFFSETS, NOW, SAST)
|
|
assert.strictEqual(winter.left[0].label, "LA")
|
|
assert.strictEqual(winter.left[0].relativeMinutes, -540)
|
|
|
|
var pst = {}
|
|
Object.keys(OFFSETS).forEach(function (k) { pst[k] = OFFSETS[k] })
|
|
pst["America/Los_Angeles"] = "-0800"
|
|
var summer = M.buildBar(CONFIG, pst, NOW, SAST)
|
|
assert.strictEqual(summer.left[0].relativeMinutes, -600)
|
|
})
|
|
|
|
test("zonesToProbe dedupes and rejects shell-unsafe names", function () {
|
|
var zones = M.zonesToProbe({
|
|
clocks: [
|
|
{ label: "A", timezone: "Europe/London" },
|
|
{ label: "B", timezone: "Europe/London" },
|
|
{ label: "C", timezone: "; rm -rf /" },
|
|
{ label: "D", timezone: "$(whoami)" },
|
|
{ label: "E", timezone: "" },
|
|
{ label: "F", timezone: "America/Argentina/Buenos_Aires" },
|
|
{ label: "G", timezone: "Etc/GMT+5" }
|
|
]
|
|
})
|
|
assert.deepStrictEqual(zones, ["Europe/London", "America/Argentina/Buenos_Aires", "Etc/GMT+5"])
|
|
})
|
|
|
|
test("parseOffsetReport reads the probe output", function () {
|
|
var got = M.parseOffsetReport("Europe/London\t+0100\nAsia/Tokyo\t+0900\n\n")
|
|
assert.deepStrictEqual(got, { "Europe/London": "+0100", "Asia/Tokyo": "+0900" })
|
|
})
|
|
|
|
test("an empty config renders an empty bar, not a crash", function () {
|
|
var bar = M.buildBar({}, {}, NOW, SAST)
|
|
assert.deepStrictEqual(bar.left, [])
|
|
assert.deepStrictEqual(bar.right, [])
|
|
assert.strictEqual(bar.center.time, "17:24")
|
|
assert.deepStrictEqual(M.buildBar(null, null, NOW, null).center.time, "15:24")
|
|
})
|
|
|
|
test("seconds are per clock, with a config-wide default", function () {
|
|
var cfg = {
|
|
showSeconds: false,
|
|
clocks: [
|
|
{ label: "LA", timezone: "America/Los_Angeles", showSeconds: true },
|
|
{ label: "NYC", timezone: "America/New_York" }
|
|
]
|
|
}
|
|
var bar = M.buildBar(cfg, OFFSETS, NOW, SAST)
|
|
var byLabel = {}
|
|
bar.left.forEach(function (c) { byLabel[c.label] = c })
|
|
assert.strictEqual(byLabel.LA.time, "08:24:00", "opted in")
|
|
assert.strictEqual(byLabel.NYC.time, "11:24", "inherits the default")
|
|
assert.strictEqual(bar.center.time, "17:24", "centre inherits the default")
|
|
})
|
|
|
|
test("an explicit false on a clock beats a true default", function () {
|
|
var cfg = {
|
|
showSeconds: true,
|
|
clocks: [
|
|
{ label: "LA", timezone: "America/Los_Angeles", showSeconds: false },
|
|
{ label: "NYC", timezone: "America/New_York" }
|
|
]
|
|
}
|
|
var bar = M.buildBar(cfg, OFFSETS, NOW, SAST)
|
|
var byLabel = {}
|
|
bar.left.forEach(function (c) { byLabel[c.label] = c })
|
|
assert.strictEqual(byLabel.LA.time, "08:24", "opted out")
|
|
assert.strictEqual(byLabel.NYC.time, "11:24:00")
|
|
assert.strictEqual(bar.center.time, "17:24:00")
|
|
})
|
|
|
|
test("the centre clock carries its own seconds override", function () {
|
|
var cfg = {
|
|
showSeconds: false,
|
|
local: { showSeconds: true },
|
|
clocks: [{ label: "LA", timezone: "America/Los_Angeles" }]
|
|
}
|
|
var bar = M.buildBar(cfg, OFFSETS, NOW, SAST)
|
|
assert.strictEqual(bar.center.time, "17:24:00")
|
|
assert.strictEqual(bar.left[0].time, "08:24", "clocks are unaffected by it")
|
|
})
|
|
|
|
test("anyShowsSeconds drives the tick rate", function () {
|
|
assert.strictEqual(M.anyShowsSeconds({ clocks: [{ label: "A", timezone: "X" }] }), false)
|
|
assert.strictEqual(M.anyShowsSeconds({ showSeconds: true, clocks: [] }), true, "the default alone counts")
|
|
assert.strictEqual(M.anyShowsSeconds({ local: { showSeconds: true }, clocks: [] }), true, "the centre alone counts")
|
|
assert.strictEqual(M.anyShowsSeconds({
|
|
clocks: [{ label: "A", timezone: "X" }, { label: "B", timezone: "Y", showSeconds: true }]
|
|
}), true, "one clock is enough")
|
|
assert.strictEqual(M.anyShowsSeconds({
|
|
showSeconds: true,
|
|
local: { showSeconds: false },
|
|
clocks: [{ label: "A", timezone: "X", showSeconds: false }]
|
|
}), false, "everything opted out")
|
|
assert.strictEqual(M.anyShowsSeconds(null), false)
|
|
})
|
|
|
|
test("an unresolved clock keeps the width of the format it asked for", function () {
|
|
var bar = M.buildBar({ clocks: [
|
|
{ label: "A", timezone: "Not/AZone" },
|
|
{ label: "B", timezone: "Not/AZone", showSeconds: true }
|
|
] }, {}, NOW, SAST)
|
|
assert.strictEqual(bar.right[0].time, "--:--")
|
|
assert.strictEqual(bar.right[1].time, "--:--:--")
|
|
})
|
|
|
|
console.log(passed + " passed" + (process.exitCode ? ", some FAILED" : ""))
|