Files
OmarchyClockBar/tests/model.test.js
T
HarveyandClaude Opus 5 8364fe2141 Seconds per clock, and type that scales with the bar height
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
2026-09-04 18:02:36 +02:00

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" : ""))