Files
omarchy/test/shell.d/clock-test.sh
T
d74d6ea249 Build the calendar month grid in UTC (#10476)
* Build the calendar month grid in UTC

The grid walked a cursor at local midnight and stepped it with setDate(getDate() + 1). Chile starts DST at 24:00 on the first Saturday of September, so local midnight of 6 September 2026 does not exist, and Qt's V4 engine resolves a missing local time backward -- onto 5 September 23:00, the same calendar date, which the next iteration reads again. September 2026 draws 30 31 1 2 3 4 5 and then 5 6 7 8 9 10 11, and every later date sits one weekday early. Cuba, Lebanon, the Azores and Egypt change over at 00:00 too and break the same way in their own transition month; where the gap lands on a leading padding cell instead, the whole six-week grid shifts by a day with no duplicate in it.

Node resolves the identical gap forward, so the suite has never seen this: running the shipped monthGrid under Node across all 598 tzdata zones for 2020-2035 reports nothing, while the same code under the QML engine breaks 402 grids. A sequence of civil dates never needed a wall clock, so it walks UTC, where every day is 24 hours long, and the test pins that by building the grid with a Date whose local constructor and accessors throw.

stepMonth reads a month back off the same kind of date. No zone puts a midnight gap on a first of the month, so it is not broken today, but it moves with the grid rather than waiting for a tzdata release to make it a second bug.

Co-Authored-By: Codex XHigh <noreply@openai.com>

* Name the calendar heading's month from local noon

The heading built a local midnight on the first of the month, and that midnight does not always exist: Cuba started DST at 00:00 on 1 April 2012, so in Havana the QML engine resolves it back onto 31 March and the heading reads March 2012 above an April grid. Noon exists on every first of the month there has been, and keeping it local is what makes Qt.formatDate print the right month in every zone.

The Apia case comes from #13186: Samoa skipped 30 December 2011 entirely, so unlike a one-hour midnight gap, Node's V8 cannot resolve its way past it, and the test fails on the old grid under Node instead of only in the shell.

Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:14:23 +02:00

323 lines
23 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const calendar = requireFromRoot('shell/plugins/panels/clock/Model.js')
const panelSource = fs.readFileSync(root + '/shell/plugins/panels/clock/Panel.qml', 'utf8')
// Comments stripped: a wiring assertion that a commented-out line can satisfy
// passes while the widget is broken.
const widgetSource = fs.readFileSync(root + '/shell/plugins/panels/clock/BarWidget.qml', 'utf8')
.replace(/^\s*\/\/.*$/gm, '')
// ---- week start resolution
assertEqual(calendar.normalizedWeekStart('monday', 0), 1, 'calendar reads a named week start')
assertEqual(calendar.normalizedWeekStart('SUN', 1), 0, 'calendar reads an abbreviated week start')
assertEqual(calendar.normalizedWeekStart(6, 1), 6, 'calendar reads a numeric week start')
assertEqual(calendar.normalizedWeekStart(null, 0), 0, 'calendar falls back to the locale first day')
assertEqual(calendar.normalizedWeekStart('garbage', 0), 0, 'calendar falls back when the setting is unparseable')
assertEqual(calendar.normalizedWeekStart(undefined, undefined), 1, 'calendar defaults to Monday without a locale')
assertEqual(calendar.weekStartSettingName(0), 'sunday', 'calendar persists Sunday by name')
assertEqual(calendar.weekStartSettingName(1), 'monday', 'calendar persists Monday by name')
assertEqual(calendar.toggledWeekStart(1), 0, 'calendar toggles Monday to Sunday')
assertEqual(calendar.toggledWeekStart(0), 1, 'calendar toggles Sunday to Monday')
assertEqual(calendar.toggledWeekStart(6), 1, 'calendar toggles an exotic week start to Monday')
assertDeepEqual(calendar.weekdayOrder(1), [1, 2, 3, 4, 5, 6, 0], 'calendar orders weekdays from Monday')
assertDeepEqual(calendar.weekdayOrder(0), [0, 1, 2, 3, 4, 5, 6], 'calendar orders weekdays from Sunday')
// ---- ISO week numbers, matching the clock widget's 'ww' token
assertEqual(calendar.isoWeek(2026, 0, 1), 1, 'calendar numbers a Thursday January 1 as week 1')
assertEqual(calendar.isoWeek(2021, 0, 1), 53, 'calendar numbers early January into the previous ISO year')
assertEqual(calendar.isoWeek(2026, 11, 31), 53, 'calendar numbers the last week of a long year')
assertEqual(calendar.isoWeek(2026, 6, 26), 30, 'calendar numbers a midsummer Sunday')
// ---- day-of-year arithmetic behind the hero stats
assertEqual(calendar.dayOfYear(2026, 6, 26), 207, 'calendar counts the day of the year')
assertEqual(calendar.yearProgressPercent(2026, 0, 1), 0, 'calendar starts the year at zero percent done')
assertEqual(calendar.yearProgressPercent(2026, 6, 26), 56, 'calendar reports the share of the year behind you')
assertEqual(calendar.yearProgressPercent(2026, 11, 31), 100, 'calendar finishes the year at a hundred percent')
assertEqual(calendar.yearProgressPercent(2024, 11, 31), 100, 'calendar finishes a leap year too')
// ---- memento mori
// A birth year rather than an age, so the bar keeps counting on its own.
assertEqual(calendar.parseBirthYear('1979', 2026), 1979, 'calendar reads a birth year')
assertEqual(calendar.parseBirthYear(1979, 2026), 1979, 'calendar reads a numeric birth year')
assertEqual(calendar.parseBirthYear(' 1979 ', 2026), 1979, 'calendar trims a birth year')
assertEqual(calendar.parseBirthYear('2026', 2026), 2026, 'calendar accepts the current year')
assertEqual(calendar.parseBirthYear('2027', 2026), 0, 'calendar rejects a birth year in the future')
assertEqual(calendar.parseBirthYear('1905', 2026), 0, 'calendar rejects an implausibly distant birth year')
assertEqual(calendar.parseBirthYear('79', 2026), 0, 'calendar rejects a two-digit year')
assertEqual(calendar.parseBirthYear('', 2026), 0, 'calendar treats a blank birth year as unset')
assertEqual(calendar.parseBirthYear('abc', 2026), 0, 'calendar treats a non-numeric birth year as unset')
assertEqual(calendar.ageFromBirthYear('1979', 2026), 47, 'calendar derives an age from a birth year')
assertEqual(calendar.ageFromBirthYear('1979', 2027), 48, 'calendar keeps counting as the years pass')
assertEqual(calendar.ageFromBirthYear('2026', 2026), 0, 'calendar makes someone born this year zero')
assertEqual(calendar.ageFromBirthYear('', 2026), 0, 'calendar derives no age without a birth year')
assertEqual(calendar.parseAge('47'), 47, 'calendar reads an age')
assertEqual(calendar.parseAge(47), 47, 'calendar reads a numeric age')
assertEqual(calendar.parseAge(' 47 '), 47, 'calendar trims an age')
assertEqual(calendar.parseAge(''), 0, 'calendar treats a blank age as unset')
assertEqual(calendar.parseAge('0'), 0, 'calendar treats zero as unset')
assertEqual(calendar.parseAge('-3'), 0, 'calendar treats a negative age as unset')
assertEqual(calendar.parseAge('121'), 0, 'calendar treats an implausible age as unset')
assertEqual(calendar.parseAge('abc'), 0, 'calendar treats a non-numeric age as unset')
assertEqual(calendar.parseAge('4.5'), 0, 'calendar treats a fractional age as unset')
assertEqual(calendar.parseLifeExpectancy('65'), 65, 'calendar reads a life expectancy')
assertEqual(calendar.parseLifeExpectancy(''), 90, 'calendar defaults the life expectancy to ninety')
assertEqual(calendar.parseLifeExpectancy(0), 90, 'calendar defaults an unset life expectancy')
assertEqual(calendar.parseLifeExpectancy('abc'), 90, 'calendar defaults a non-numeric life expectancy')
assertEqual(calendar.parseLifeExpectancy('200'), 90, 'calendar defaults an implausible life expectancy')
assertEqual(calendar.lifeProgressPercent(45, 90), 50, 'calendar measures a life against the expectancy given')
assertEqual(calendar.lifeProgressPercent(45, 65), 69, 'calendar honours a shorter expectancy')
assertEqual(calendar.lifeProgressPercent(45, ''), 50, 'calendar falls back to ninety when no expectancy is set')
assertEqual(calendar.lifeProgressPercent(90, 90), 100, 'calendar fills the life bar at the expectancy')
assertEqual(calendar.lifeProgressPercent(80, 65), 100, 'calendar never overfills the life bar')
assertEqual(calendar.lifeProgressPercent(0, 90), 0, 'calendar leaves the life bar empty when no age is set')
assertEqual(calendar.daysInYear(2024), 366, 'calendar knows leap years are longer')
assertEqual(calendar.daysInYear(2026), 365, 'calendar knows common years')
// ---- month grid
const july = calendar.monthGrid(2026, 6, 1, '2026-07-26')
assertEqual(july.length, 6, 'calendar always draws six week rows')
assert(july.every(week => week.days.length === 7), 'calendar always draws seven day columns')
assertDeepEqual(july.map(week => week.week), [27, 28, 29, 30, 31, 32], 'calendar numbers every row by ISO week')
assertDeepEqual(july[0].days.map(day => day.day), [29, 30, 1, 2, 3, 4, 5], 'calendar pads the first row with the previous month')
assertDeepEqual(july[0].days.map(day => day.inMonth), [false, false, true, true, true, true, true], 'calendar marks padding days as outside the month')
assertDeepEqual(july[0].days.map(day => day.weekend), [false, false, false, false, false, true, true], 'calendar marks Saturday and Sunday as the weekend')
assertDeepEqual(
july.flatMap(week => week.days).filter(day => day.today).map(day => day.key),
['2026-07-26'],
'calendar marks exactly one cell as today'
)
assert(july.flatMap(week => week.days).every(day => day.cursor === undefined), 'calendar grid carries no selection state')
// Both conventions land on the same ISO week numbers, because every row is
// numbered by the ISO week owning its Thursday.
const julySunday = calendar.monthGrid(2026, 6, 0, '')
assertDeepEqual(julySunday[0].days.map(day => day.day), [28, 29, 30, 1, 2, 3, 4], 'calendar shifts the grid for a Sunday week start')
assertDeepEqual(julySunday.map(week => week.week), [27, 28, 29, 30, 31, 32], 'calendar keeps ISO week numbers across week starts')
const januarySunday = calendar.monthGrid(2021, 0, 0, '')
assertEqual(januarySunday[0].week, 53, 'calendar carries the previous ISO year into a straddling first row')
// Zones that start DST at 00:00 have a calendar day with no local midnight, and
// the engines disagree about which side of the gap it falls on: V4 resolves it
// backward onto the previous date, V8 forward. Node therefore cannot see the
// duplicated day the shell draws, so pin the property that makes the question
// moot -- the grid is walked in UTC and never asks for a wall clock.
const RealDate = Date
class LocalTimeIsForbidden extends RealDate {
constructor(...args) {
if (args.length > 1) throw new Error('built a Date from local components')
super(...args)
}
getFullYear() { throw new Error('read the local year') }
getMonth() { throw new Error('read the local month') }
getDate() { throw new Error('read the local day') }
getDay() { throw new Error('read the local weekday') }
setDate() { throw new Error('stepped the local day') }
}
function withoutLocalTime(build) {
global.Date = LocalTimeIsForbidden
try {
return { value: build(), localTimeUse: '' }
} catch (error) {
return { value: null, localTimeUse: error.message }
} finally {
global.Date = RealDate
}
}
const september = withoutLocalTime(() => calendar.monthGrid(2026, 8, 0, ''))
assertEqual(september.localTimeUse, '', 'calendar builds the month grid without reading local time')
assertDeepEqual(
september.value.slice(0, 2).flatMap(week => week.days).map(day => day.day),
[30, 31, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
'calendar runs September 2026 straight through the Chilean DST midnight'
)
const stepped = withoutLocalTime(() => calendar.stepMonth(2026, 8, 1))
assertEqual(stepped.localTimeUse, '', 'calendar steps months without reading local time')
assertDeepEqual(stepped.value, { year: 2026, month: 9 }, 'calendar steps from September to October')
// Samoa skipped 30 December 2011 outright, a gap even V8 cannot paper over.
const previousTZ = process.env.TZ
process.env.TZ = 'Pacific/Apia'
const apia = calendar.monthGrid(2011, 11, 1, '')
if (previousTZ === undefined) delete process.env.TZ
else process.env.TZ = previousTZ
assertDeepEqual(apia[4].days.map(day => day.day), [26, 27, 28, 29, 30, 31, 1], 'calendar keeps a day the local zone skipped')
assert(/viewDate: new Date\(viewYear, viewMonth, 1, 12\)/.test(panelSource), 'calendar names the heading month from a noon that always exists')
// ---- stepping
assertDeepEqual(calendar.stepMonth(2026, 0, 1), { year: 2026, month: 1 }, 'calendar steps to the next month')
assertDeepEqual(calendar.stepMonth(2026, 0, -1), { year: 2025, month: 11 }, 'calendar steps back across the new year')
assertDeepEqual(calendar.stepMonth(2026, 11, 1), { year: 2027, month: 0 }, 'calendar steps forward across the new year')
assertDeepEqual(calendar.stepMonth(2026, 6, 12), { year: 2027, month: 6 }, 'calendar steps a whole year at a time')
assertEqual(calendar.dateKey(2026, 0, 5), '2026-01-05', 'calendar zero-pads date keys')
assertEqual(calendar.keyForDate(new Date(2026, 6, 26)), '2026-07-26', 'calendar keys a Date the same way')
// ---- bar label format ring
// The order must not depend on which entry is current: cycling writes the
// result back to shell.json, so a self-reordering ring would ping-pong.
const ring = calendar.clockFormatRing('dddd HH:mm', "d MMMM 'W'ww yyyy", calendar.clockFormats(false))
assertDeepEqual(ring, calendar.clockFormats(false), 'clock rings the stock presets in their own order')
assertEqual(new Set(ring).size, ring.length, 'clock never offers the same format twice')
assertDeepEqual(
calendar.clockFormatRing("d MMMM 'W'ww yyyy", 'dddd HH:mm', calendar.clockFormats(false)),
ring,
'clock keeps the ring order steady as the current format moves through it'
)
assertDeepEqual(
calendar.clockFormatRing('HH:mm:ss', '', ['HH:mm', 'dddd HH:mm']),
['HH:mm', 'dddd HH:mm', 'HH:mm:ss'],
'clock appends a hand-written format to the end of the ring'
)
assertDeepEqual(calendar.clockFormatRing('', '', []), ['HH:mm'], 'clock keeps a usable format when nothing is configured')
assertEqual(calendar.nextClockFormat(ring, ring[0]), ring[1], 'clock steps to the next format')
assertEqual(calendar.nextClockFormat(ring, ring[ring.length - 1]), ring[0], 'clock wraps the format ring')
assertEqual(calendar.nextClockFormat(ring, 'HH:mm:ss'), ring[0], 'clock starts at the top from a format outside the ring')
// Both rings, contents and order: a right click walks this list and writes
// the result back to shell.json, so an inserted preset should have to be
// acknowledged here.
assertDeepEqual(
calendar.clockFormats(false),
[
'dddd HH:mm', 'dddd h:mm AP',
'dddd HH:mm:ss', 'dddd h:mm:ss AP',
'HH:mm', 'h:mm AP',
'ddd d MMM HH:mm', 'ddd d MMM h:mm AP',
"d MMMM 'W'ww yyyy",
// No twin: ISO 8601 writes time on a 24-hour clock, so an AM/PM variant
// would contradict the one thing that format is for.
'yyyy-MM-dd HH:mm'
],
'clock offers the horizontal presets in this order'
)
assertDeepEqual(
calendar.clockFormats(true),
['HH\n\u2014\nmm', 'h\n\u2014\nmm\nAP', "dd\nMMM\n'W'ww\n''yy", 'HH\nmm'],
'clock offers the stacked presets in this order'
)
assertEqual(
calendar.nextClockFormat(ring, 'dddd HH:mm'),
'dddd h:mm AP',
'clock reaches an AM/PM twin in one right click'
)
assertEqual(calendar.isoWeekLiteral(2026, 0, 5), '02', 'clock zero-pads the ISO week token')
// ---- seconds detection, which decides how often the widget's clock ticks
assert(calendar.clockNeedsSeconds('dddd HH:mm:ss'), 'clock sees seconds in the live preset')
assert(calendar.clockNeedsSeconds('h:mm:ss AP'), 'clock sees seconds in an AM/PM format')
assert(!calendar.clockNeedsSeconds('dddd HH:mm'), 'clock sees no seconds in a minute format')
assert(!calendar.clockNeedsSeconds("d MMMM 'W'ww yyyy"), 'clock sees no seconds in the long date format')
assert(!calendar.clockNeedsSeconds("dd\nMMM\n'W'ww\n''yy"), 'clock sees no seconds in the stacked date format')
assert(!calendar.clockNeedsSeconds("HH:mm 'since'"), 'clock reads an s inside a quoted literal as text')
// Qt reads an opening quote with no closing one as a literal running to the end
// of the format, so "HH:mm 'sec" prints "09:39 sec" and never a second count.
assert(!calendar.clockNeedsSeconds("HH:mm 'sec"), 'clock reads an unterminated literal as text to the end')
assert(!calendar.clockNeedsSeconds("HH:mm's"), 'clock reads a trailing unterminated literal as text')
// The doubled quote is a literal apostrophe rather than an empty literal, so
// the s after it is still the seconds token: Qt prints "'23".
assert(calendar.clockNeedsSeconds("''s"), 'clock still sees seconds after an escaped apostrophe')
assert(!calendar.clockNeedsSeconds(''), 'clock sees no seconds in an empty format')
assert(!calendar.clockNeedsSeconds(null), 'clock sees no seconds in a missing format')
// ---- widget wiring
assert(/moduleName: "omarchy\.clock"/.test(panelSource), 'calendar panel declares its module name')
assert(/ipcTarget: "omarchy\.clock"/.test(panelSource), 'calendar panel registers its IPC target')
assert(/manageIpc: false/.test(panelSource), 'calendar panel leaves the IPC target to the bar widget')
assert(/anchorItem: root\.anchorItem/.test(panelSource), 'calendar panel anchors to the host widget button')
assert(/function toggleWeekStart\(\)/.test(panelSource), 'calendar panel exposes a week start toggle')
assert(/function toggleWeekStart\(\): void \{ root\.toggleWeekStart\(\) \}/.test(widgetSource), 'clock exposes the week start toggle over IPC')
assert(/precision: root\.showsSeconds \? SystemClock\.Seconds : SystemClock\.Minutes/.test(widgetSource), 'clock ticks per second only for a format that prints seconds')
// Both halves of the tick, because either one alone can be true while the
// label sits frozen: the precision must follow the format, and every tick
// must reach the label.
assert(/showsSeconds: Model\.clockNeedsSeconds\(activeFormat\)/.test(widgetSource), 'clock decides its tick rate from the format it is showing')
assert(/onDateChanged: root\.displayDate = date/.test(widgetSource), 'clock repaints the label on every tick')
assert(/setting\("weekStartDay", null\)/.test(panelSource) && /persistSettings\(\{ weekStartDay:/.test(panelSource), 'calendar reads and writes the week start as weekStartDay')
assert(/updateEntryInline/.test(panelSource), 'calendar panel persists the week start to shell.json')
assert(/function moveMonth\(delta\)/.test(panelSource), 'calendar panel steps between months')
assert(!/property bool onToday/.test(panelSource) && !/root\.onToday/.test(panelSource), 'calendar panel avoids the on-prefixed property name QML reads as a signal handler')
assert(/readonly property bool viewingCurrentMonth:/.test(panelSource), 'calendar panel tracks whether the current month is on screen')
assert(!/MouseArea/.test(panelSource.slice(panelSource.indexOf('model: modelData.days'), panelSource.indexOf('// Hairline'))), 'calendar day cells are not selectable')
assert(/yearDone: Model\.yearProgress\(today\./.test(panelSource), 'calendar year bar stays pinned to today while months are stepped')
assert(/Qt\.callLater\(function\(\) \{\s*\n\s*if \(root\.opened\) setCenterHoverRevealSuppressed\(true\)/.test(panelSource), 'calendar claims the shared hover-reveal flag after the popout handoff, so the panel taking over wins')
assert(/function close\(\) \{\s*\n\s*setCenterHoverRevealSuppressed\(false\)/.test(panelSource), 'calendar always releases the shared hover-reveal flag on close')
assert(/width: Math\.max\(calendarScroll\.width, gridColumn\.width\)/.test(panelSource), 'calendar scrolls rather than clipping the grid on a narrow popup')
assert(/enabled: !root\.viewingCurrentMonth/.test(panelSource) && /onClicked: root\.goToToday\(\)/.test(panelSource), 'calendar hero returns to today once the view has stepped away')
assert(!/clampMonth/.test(panelSource), 'calendar steps freely into future months')
assert(/Qt\.formatDate\(root\.today, "MMMM d"\)/.test(panelSource), 'calendar hero spells out today')
assert(/id: yearLabel/.test(panelSource) && /root\.yearDone/.test(panelSource), 'calendar panel shows the year progress bar')
// The memento mori bar is opt-in: double-tapping the year bar asks for an age,
// and nothing shows until one has been given.
assert(/onDoubleTapped: root\.startEditingLife\(\)/.test(panelSource), 'calendar asks for an age when the year bar is double-tapped')
assert(/persistSettings\(\{ birthYear: born, lifeExpectancy: span \}\)/.test(panelSource), 'calendar saves birth year and expectancy together, so neither lands on a stale copy')
assert(/readonly property int birthYear: Model\.parseBirthYear\(setting\("birthYear", 0\)/.test(panelSource), 'calendar reads the saved birth year back')
assert(/readonly property int age: Model\.ageFromBirthYear\(birthYear/.test(panelSource), 'calendar derives the age from the stored birth year')
assert(/readonly property int lifeExpectancy: Model\.parseLifeExpectancy\(setting\("lifeExpectancy", 0\)\)/.test(panelSource), 'calendar reads the saved expectancy back')
assert(/id: expectancyField/.test(panelSource) && /id: bornField/.test(panelSource), 'calendar offers both inputs')
assert(/visible: root\.editingLife\s*\n\s*anchors\.horizontalCenter: parent\.horizontalCenter/.test(panelSource), 'calendar centers the inputs over the bar they replace')
assert(/visible: root\.birthYear > 0/.test(panelSource), 'calendar hides the life bar until a birth year is known')
assert(/text: "LIFE"/.test(panelSource) && /root\.lifeDone/.test(panelSource), 'calendar shows the life bar')
assert(/text: "Memento Mori"/.test(panelSource), 'calendar names the life bar on hover')
assert(/onDoubleTapped: root\.clearLife\(\)/.test(panelSource), 'calendar puts the life bar away when it is double-tapped')
assert(/persistSettings\(\{ birthYear: 0 \}\)/.test(panelSource), 'calendar clears the birth year to hide the life bar')
assertEqual(calendar.parseBirthYear(0, 2026), 0, 'a cleared birth year reads back as unset')
assert(/blocked: root\.editingLife/.test(panelSource), 'calendar lets the inputs have the keyboard while they are up')
assert(/if \(root\.editingLife\) root\.cancelEditingLife\(\)/.test(panelSource), 'calendar drops a half-finished edit when the panel closes')
assert(/source: Qt\.resolvedUrl\("Panel\.qml"\)/.test(widgetSource), 'clock widget hosts the calendar panel')
assert(/readonly property bool opened:/.test(widgetSource), 'clock widget exposes the panel open state to shell routing')
assert(/Qt\.RightButton\) root\.cycleFormat\(\)/.test(widgetSource), 'clock right click cycles the label format')
assert(/readonly property string activeFormat: configuredFormat/.test(widgetSource), 'clock shows the format it has stored')
assert(/entry\[vertical \? "verticalFormat" : "format"\] = next/.test(widgetSource) && /updateEntryInline/.test(widgetSource), 'clock writes a cycled format back to shell.json')
assert(!/formatIndex/.test(widgetSource), 'clock keeps no session-only format position')
assert(/else root\.togglePanel\(\)/.test(widgetSource), 'clock left click reveals the calendar')
assert(/omarchy-menu-timezone/.test(widgetSource), 'clock keeps the timezone picker on middle click')
// The bar identifies a panel by the widget in its slot, so the nested panel
// has to present the host widget rather than itself.
assert(/owner: root\.barIdentity/.test(panelSource), 'calendar panel gives the bar its host widget as popout identity')
assert(/switchPanelFrom\(root\.barIdentity, direction\)/.test(panelSource), 'calendar panel switches panels as its host widget')
assert(/hostWidget" in target\) target\.hostWidget = root/.test(widgetSource), 'clock widget injects itself as the panel host')
assert(/readonly property bool popoutSwitchClosing:/.test(widgetSource) && /function closeForPopoutSwitch\(\)/.test(widgetSource), 'clock widget forwards the popout-switch handshake')
assert(/openPanelIndicatorWidth: button\.labelWidth/.test(widgetSource), 'clock sizes the open-panel dot to its label')
assert(/openPanelIndicatorHeight: Math\.max/.test(widgetSource), 'clock sizes the vertical open-panel dot to one stacked line')
const barSource = fs.readFileSync(root + '/shell/plugins/bar/Bar.qml', 'utf8')
assert(/openPanelIndicatorWidth/.test(barSource) && /openPanelIndicatorHeight/.test(barSource), 'bar honours a widget-supplied open-panel dot size on both axes')
assert(/height: root\.vertical \? slot\.panelIndicatorExtent : Style\.space\(2\)/.test(barSource), 'bar sizes the vertical dot from the same hint as the horizontal one')
assert(/readonly property real labelWidth:/.test(fs.readFileSync(root + '/shell/Ui/WidgetButton.qml', 'utf8')), 'widget buttons expose their painted label width')
// A horizontal wheel reports angleDelta.y === 0; without the guard every one
// of them would read as a forward step.
assert(/if \(event\.angleDelta\.y === 0\) return/.test(panelSource), 'calendar ignores wheel events with no vertical delta')
JS
shell_json=$(cd "$ROOT" && jq -r '[.bar.layout.center[].id] | join(",")' config/omarchy/shell.json)
[[ $shell_json == *"omarchy.clock"* ]] || fail "default bar layout includes the clock widget" "center: $shell_json"
[[ $shell_json != *"omarchy.calendar"* ]] || fail "clock hosts the calendar instead of a second bar pill" "center: $shell_json"
pass "default bar layout includes the clock widget"
grep -q 'o.bind("SUPER + CTRL + ALT + D", "Calendar", { panel = "omarchy.clock" })' \
"$ROOT/default/hypr/bindings/utilities.lua" ||
fail "SUPER+CTRL+ALT+D toggles the calendar panel"
pass "SUPER+CTRL+ALT+D toggles the calendar panel"