Files
omarchy/plugins/io.github.elevate08.qs-bitwarden-cli/tests/settings-screen.test.js
T
asepharyana 1cdb82a76f Sync config from arch
- 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
2026-09-23 15:19:12 +07:00

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)
}