- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
268 lines
13 KiB
JavaScript
268 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
// The settings screen's structure -- what is pinned, what scrolls, how its
|
|
// sections are drawn -- and the one invariant that is panel-wide rather than
|
|
// settings-only: every scrolling view keeps its content clear of its own
|
|
// scrollbar.
|
|
//
|
|
// node tests/settings-screen.test.js
|
|
|
|
const fs = require("fs")
|
|
const path = require("path")
|
|
|
|
const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8")
|
|
|
|
let pass = 0
|
|
const failures = []
|
|
const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`)
|
|
|
|
// The settings screen, from its wrapper Column to the end of the Flickable.
|
|
const screenAt = panelSrc.indexOf("id: settingsScreen")
|
|
const flickAt = panelSrc.indexOf("id: settingsFlick")
|
|
const colAt = panelSrc.indexOf("id: settingsCol")
|
|
const screen = screenAt < 0 ? "" : panelSrc.slice(screenAt, panelSrc.indexOf("SCREEN 1", screenAt))
|
|
|
|
const shieldAt = panelSrc.indexOf("id: shieldIconComp")
|
|
const statusBarAt = panelSrc.indexOf("// Status Bar Button", shieldAt)
|
|
const shield = shieldAt < 0 ? "" : panelSrc.slice(shieldAt, statusBarAt)
|
|
|
|
// --- colorized menu-bar icon ------------------------------------------------
|
|
|
|
check("colorized icon reads the persisted boolean setting",
|
|
/readonly property bool colorizeIcon: Model\.boolSetting\("colorizeIcon", setting\("colorizeIcon", false\)\)/.test(panelSrc),
|
|
"expected a false-safe colorizeIcon setting property")
|
|
|
|
check("colorized icon uses the theme accent only when enabled",
|
|
/color:\s*root\.colorizeIcon \? Color\.accent : \(bar \? bar\.barForeground : Color\.foreground\)/.test(shield),
|
|
"expected the primary shield to select Color.accent or its existing foreground")
|
|
|
|
check("colorized icon leaves status badges independent",
|
|
shield.includes("color: bar ? bar.urgent : Color.urgent")
|
|
&& shield.includes("color: bar ? bar.barForeground : Color.foreground"),
|
|
"expected urgent and locked badge colors to remain independently bound")
|
|
|
|
check("panel-open indicator keeps Omarchy's standard width",
|
|
!panelSrc.includes("openPanelIndicatorWidth"),
|
|
"expected no plugin-specific width override for the panel-open indicator")
|
|
|
|
check("custom shield corrects its painted side bearings",
|
|
shield.includes("id: shieldGlyphMetrics")
|
|
&& shield.includes("shieldGlyphMetrics.tightBoundingRect")
|
|
&& shield.includes("anchors.horizontalCenterOffset"),
|
|
"expected corrected painted side bearings on the shield")
|
|
|
|
// The centering above is what aligns the glyph with the panel-open indicator;
|
|
// the renderer is not part of it -- both put the painted center on the same
|
|
// pixel at scale 1.3333. QtRendering additionally drew saturated colour along
|
|
// the glyph edges, which no other icon in the bar has, so the shield renders
|
|
// the way the rest of Omarchy does.
|
|
check("shield renders the way the rest of the bar does",
|
|
shield.includes("renderType: Text.NativeRendering")
|
|
&& !shield.includes("renderType: Text.QtRendering"),
|
|
"expected the shield to use Text.NativeRendering, as Omarchy's own glyphs do")
|
|
|
|
check("the settings screen has a wrapper outside the scroll area", screenAt >= 0,
|
|
"expected a settingsScreen Column")
|
|
|
|
// --- what must not scroll away -----------------------------------------------
|
|
|
|
check("the way out is pinned, not scrolled",
|
|
screenAt < flickAt && screen.indexOf('text: "Back (Esc)"') < screen.indexOf("id: settingsFlick"),
|
|
"the Back button must sit above the Flickable, not inside settingsCol")
|
|
|
|
check("the pinned section indicator is also above the scroll area",
|
|
screen.indexOf("id: stickySection") >= 0
|
|
&& screen.indexOf("id: stickySection") < screen.indexOf("id: settingsFlick"),
|
|
"stickySection must be outside the Flickable")
|
|
|
|
check("the scrolling column no longer draws its own Back button",
|
|
panelSrc.slice(colAt).indexOf('text: "Back (Esc)"') < 0
|
|
|| panelSrc.slice(colAt).indexOf('text: "Back (Esc)"') > panelSrc.slice(colAt).indexOf("SCREEN 1"),
|
|
"settingsCol still contains a Back button")
|
|
|
|
// The section name goes left, the way out goes right.
|
|
check("the section indicator anchors left and the exit anchors right",
|
|
/id: stickySection[\s\S]{0,200}anchors\.left: parent\.left/.test(screen)
|
|
&& screen.indexOf("anchors.right: parent.right") < screen.indexOf('text: "Back (Esc)"')
|
|
&& screen.indexOf("anchors.right: parent.right") > screen.indexOf("id: stickySection"),
|
|
"expected section on the left, Back on the right")
|
|
|
|
// --- the pinned indicator tracks the scroll ----------------------------------
|
|
|
|
check("the indicator is recomputed as the view scrolls",
|
|
/onContentYChanged: root\.updateSettingsSticky\(\)/.test(panelSrc),
|
|
"scrolling must update which section the bar names")
|
|
|
|
check("and when folding changes what is in the list",
|
|
/onContentHeightChanged: Qt\.callLater\(root\.updateSettingsSticky\)/.test(panelSrc),
|
|
"a fold changes contentHeight and must re-run the check after layout")
|
|
|
|
check("the indicator is held rather than bound",
|
|
/property var settingsStickyEntry: null/.test(panelSrc),
|
|
"it depends on delegate geometry, which a binding cannot read without fighting layout")
|
|
|
|
// The bar names the section the view is inside, including at rest -- an empty
|
|
// bar on the one position everybody starts from is worse than a redundant one.
|
|
// Duplication is prevented at the other end instead: the in-list heading of
|
|
// the section the bar names is drawn transparent.
|
|
check("the bar names a section from the top of the list, before any scrolling",
|
|
/if \(row\.y > top \+ 1\) break/.test(panelSrc),
|
|
"a heading at the top edge is the section the view is in")
|
|
|
|
check("the in-list heading yields to the bar rather than drawing alongside it",
|
|
/readonly property bool yieldsToBar: isGroup[\s\S]{0,140}root\.settingsStickyEntry\.group === modelData\.group/.test(panelSrc),
|
|
"the pinned section's own heading must not be drawn twice")
|
|
|
|
// Going transparent hid the ink and kept the space, which left a
|
|
// heading-sized hole directly under the bar. It gives up the row instead.
|
|
check("it yields its space, not just its ink",
|
|
/visible: isGroup && !yieldsToBar/.test(panelSrc)
|
|
&& !/opacity: \(root\.settingsStickyEntry/.test(panelSrc),
|
|
"a transparent row leaves a gap where the heading was")
|
|
|
|
check("the gap above it goes too",
|
|
/visible: isGroup && index > 0 && !yieldsToBar/.test(panelSrc),
|
|
"the spacer above a hidden heading would leave a smaller hole in its place")
|
|
|
|
// Exactly one heading is ever yielding, so the content height is constant:
|
|
// the one taking over collapses as the previous one is restored.
|
|
check("only the pinned section's heading yields, so the height stays constant",
|
|
/Exactly one heading is ever in this state/.test(panelSrc),
|
|
"expected the reasoning recorded where the next reader will be standing")
|
|
|
|
check("and only while part of that section is still on screen",
|
|
/if \(top < settingsSectionEnd\(i\)\)/.test(panelSrc),
|
|
"past the end of a section the bar must let go of it")
|
|
|
|
check("a section's extent is the next heading, or the last row for the final one",
|
|
/function settingsSectionEnd\(index\)/.test(panelSrc)
|
|
&& /if \(next\) return next\.y/.test(panelSrc)
|
|
&& /if \(last\) return last\.y \+ last\.height/.test(panelSrc),
|
|
"expected both the between-headings and the final-section cases")
|
|
|
|
// The trailing maintenance and danger-zone blocks are not foldable sections.
|
|
// Leaving the last group pinned through them would offer to fold something the
|
|
// user had scrolled past and could no longer see.
|
|
check("the bar empties rather than naming a section that is no longer in view",
|
|
/var found = null/.test(panelSrc)
|
|
&& !/if \(!found\) \{/.test(panelSrc),
|
|
"there must be no fallback that forces a section into an empty bar")
|
|
|
|
// --- the sections are not foldable ------------------------------------------
|
|
//
|
|
// They were, for a few commits. Three groups of three, seven and four rows do
|
|
// not need folding, and a fold is one more state to be in and one more thing
|
|
// to leave shut by accident. These assertions exist so it does not creep back
|
|
// halfway -- a chevron with nothing behind it, or a heading that swallows a
|
|
// click.
|
|
|
|
check("no collapse state is kept",
|
|
!/collapsedGroups/.test(panelSrc), "settings sections are not foldable")
|
|
|
|
check("headings are not controls",
|
|
!/toggleSettingsGroup|toggleStickySettingsGroup/.test(panelSrc),
|
|
"a heading that folds nothing must not accept a click")
|
|
|
|
check("the pinned indicator carries no chevron or count",
|
|
!/settingsStickyEntry\.collapsed|settingsStickyEntry\.count/.test(panelSrc),
|
|
"the bar names the section and nothing more")
|
|
|
|
// A heading is in the list so the indicator has geometry to read, but it is
|
|
// not something the cursor can act on -- stopping there and doing nothing on
|
|
// Enter is worse than stepping over it.
|
|
check("the keyboard cursor steps over headings",
|
|
/while \(i >= 0 && i < n && settingsEntries\[i\] && settingsEntries\[i\]\.kind === "group"\) i \+= step/.test(panelSrc),
|
|
"expected the cursor to skip group rows")
|
|
|
|
check("the screen opens on a setting, not on a heading",
|
|
/settingsIndex = firstSettingIndex\(\)/.test(panelSrc),
|
|
"the first row in the list is a heading")
|
|
|
|
check("activation and adjustment have no group case left",
|
|
!/e\.kind === "group"/.test(panelSrc),
|
|
"the cursor can no longer land on a heading, so neither needs to handle one")
|
|
|
|
// --- geometry access is funnelled ---------------------------------------------
|
|
|
|
check("view geometry is reached through named helpers, not ids scattered about",
|
|
/function settingsViewportTop\(\)/.test(panelSrc)
|
|
&& /function settingsRepeaterItem\(i\)/.test(panelSrc),
|
|
"expected settingsViewportTop and settingsRepeaterItem")
|
|
|
|
check("both helpers survive being called before the view exists",
|
|
/return settingsFlick \? settingsFlick\.contentY : 0/.test(panelSrc)
|
|
&& /return settingsRepeater \? settingsRepeater\.itemAt\(i\) : null/.test(panelSrc),
|
|
"openSettings runs before the screen is built")
|
|
|
|
// --- every scrollbar gets a lane of its own ----------------------------------
|
|
//
|
|
// These bars are overlays. Left alone each one draws on top of whatever is at
|
|
// the right edge of its view -- toggles, number fields, copy buttons, the ends
|
|
// of elided text. Every scrolling view subtracts one shared gutter, so no bar
|
|
// covers a control and the right-hand edges line up across screens.
|
|
|
|
check("the gutter is measured from a real scrollbar, not guessed",
|
|
/settingsScrollBar \? settingsScrollBar\.implicitWidth : 0/.test(panelSrc),
|
|
"a theme with a wider bar would put it back over the controls")
|
|
|
|
check("the gutter has a floor for a null bar and for the frames before layout",
|
|
/Math\.max\(settingsScrollBar[\s\S]{0,120}Style\.space\(10\)\)/.test(panelSrc),
|
|
"expected a minimum gutter")
|
|
|
|
// Every scrolling view in the panel, by the id its content width is bound to.
|
|
const scrollViews = [
|
|
"sendFlick", "fpFlick", "genFlick", "pinFlick", "setupFlick", "settingsFlick",
|
|
"filterOptionsList", "detailFlickable", "editFlickable",
|
|
"folderPickList", "orgPickList", "collectionList",
|
|
]
|
|
for (const view of scrollViews) {
|
|
check(`${view} keeps its content clear of the scrollbar`,
|
|
new RegExp(`width: ${view}\\.width - root\\.scrollGutter`).test(panelSrc),
|
|
`${view} content runs under its own scrollbar`)
|
|
}
|
|
|
|
// The vault list is a ListView, so its delegate takes the width directly
|
|
// rather than through a content column.
|
|
check("the vault list's rows keep clear of the scrollbar too",
|
|
/width: ListView\.view\.width - root\.scrollGutter/.test(panelSrc),
|
|
"the item rows run under the bar")
|
|
|
|
check("every scrolling view is accounted for",
|
|
(panelSrc.match(/ScrollBar\.vertical:/g) || []).length === scrollViews.length + 1,
|
|
`${(panelSrc.match(/ScrollBar\.vertical:/g) || []).length} scrollbars for ${scrollViews.length} views plus the list`)
|
|
|
|
check("the pinned settings row is inset to match its rows",
|
|
/anchors\.rightMargin: root\.scrollGutter/.test(panelSrc),
|
|
"Back would otherwise overhang every control beneath it")
|
|
|
|
// Heading rows carry no description and no zeroLabel, and QML evaluates the
|
|
// bindings of invisible items, so these ran for every heading in the list.
|
|
check("bindings that also run for heading rows tolerate the missing fields",
|
|
/\(modelData\.description \|\| ""\)/.test(panelSrc)
|
|
&& /\(modelData\.zeroLabel \|\| ""\)/.test(panelSrc),
|
|
"undefined reaching a QString property is a warning on every frame")
|
|
|
|
// --- the danger zone ----------------------------------------------------------
|
|
|
|
check("destructive actions are separated from maintenance ones",
|
|
/text: "MAINTENANCE"/.test(panelSrc) && /text: "DANGER ZONE"/.test(panelSrc),
|
|
"expected both headings")
|
|
|
|
check("the danger heading is drawn in the urgent colour",
|
|
/text: "DANGER ZONE"[\s\S]{0,120}foreground: Color\.urgent/.test(panelSrc),
|
|
"a destructive section should not look like every other heading")
|
|
|
|
check("the danger zone is set off by a separator",
|
|
/PanelSeparator \{ width: parent\.width \}\s*\n\s*\n?\s*PanelSectionHeader \{[\s\S]{0,120}DANGER ZONE/.test(panelSrc),
|
|
"expected a rule above the destructive section")
|
|
|
|
check("Remove Plugin Data sits under the danger heading, not beside Dependencies",
|
|
panelSrc.indexOf('text: "DANGER ZONE"') < panelSrc.indexOf('text: "Remove Plugin Data"')
|
|
&& panelSrc.indexOf('text: "Dependencies"') < panelSrc.indexOf('text: "DANGER ZONE"'),
|
|
"ordering puts the destructive button in the wrong section")
|
|
|
|
console.log(`${pass} passed, ${failures.length} failed`)
|
|
if (failures.length) {
|
|
console.error("\nFAILURES:\n " + failures.join("\n "))
|
|
process.exit(1)
|
|
}
|