Files
omarchy/test/shell.d/video-background-test.sh
T

341 lines
17 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 utilQml = fs.readFileSync(path.join(root, 'shell/Commons/Util.qml'), 'utf8')
const mediaQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundMedia.qml'), 'utf8')
const backgroundQml = fs.readFileSync(path.join(root, 'shell/plugins/background/Background.qml'), 'utf8')
const lockQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
const lockFeedQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockFeedSurface.qml'), 'utf8')
const themeSwitcher = fs.readFileSync(path.join(root, 'bin/omarchy-theme-switcher'), 'utf8')
const quattroUpgrade = fs.readFileSync(path.join(root, 'bin/omarchy-upgrade-to-quattro'), 'utf8')
const barTextColor = fs.readFileSync(path.join(root, 'bin/omarchy-bar-text-color'), 'utf8')
const menuImages = fs.readFileSync(path.join(root, 'bin/omarchy-menu-images'), 'utf8')
const lockService = fs.readFileSync(path.join(root, 'shell/plugins/lock/Service.qml'), 'utf8')
const batteryService = fs.readFileSync(path.join(root, 'shell/plugins/services/battery/Service.qml'), 'utf8')
const themeSet = fs.readFileSync(path.join(root, 'bin/omarchy-theme-set'), 'utf8')
const directImageList = fs.readFileSync(path.join(root, 'shell/plugins/image-picker/list.sh'), 'utf8')
assert(
/function isVideoPath\(path\)[\s\S]*\.test\(String\(path \|\| ""\)\)/.test(utilQml) &&
!utilQml.includes('split(/[?#]/)'),
'shared media helper identifies video paths without truncating valid local names'
)
assert(
!fs.existsSync(path.join(root, 'shell/Ui/BackgroundVideo.qml')),
'the shell keeps no video player of its own'
)
assert(
/imageUrl: path && !video \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) &&
/active: root\.path !== "" && !root\.video/.test(mediaQml) &&
!mediaQml.includes('videoUrl') &&
!mediaQml.includes('videoLoader') &&
!mediaQml.includes('videoEnabled'),
'background media is image-only: it loads stills and leaves videos to OWE'
)
assert(
!/^\s*import QtMultimedia/m.test(mediaQml) &&
!/^\s*import QtMultimedia/m.test(backgroundQml) &&
!/^\s*import QtMultimedia/m.test(lockQml) &&
!backgroundQml.includes('MediaPlayer') &&
!lockQml.includes('MediaPlayer'),
'no shell surface builds a Qt Multimedia pipeline'
)
assert(
/^import Owe\.LockFeed$/m.test(lockFeedQml) &&
lockFeedQml.includes('LockFeed {') &&
lockQml.includes('source: "LockFeedSurface.qml"') &&
lockQml.includes('active: root.feedActive') &&
/feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml),
'the lock screen shows video through the OWE lock feed, loaded so a missing module costs only the video'
)
assert(
!/^import Owe\./m.test(lockQml),
'the lock view itself carries no foreign import, so the lock still loads without the feed module'
)
assert(
lockQml.includes('path: root.loadBackground ? (root.video ? root.videoPosterPath : root.backgroundPath) : ""') &&
lockQml.indexOf('id: feedLoader') > lockQml.indexOf('MultiEffect {') &&
lockQml.includes('visible: root.video') &&
!lockQml.includes('wallpaper.video'),
'the lock screen keeps its image effect for stills and shows the feed for videos'
)
assert(
!mediaQml.includes('mipmap'),
'the shared image path leaves mipmapping off, as the desktop background had it'
)
assert(
/instant \|\| !displayedBackground \|\| isVideo\(path\) \|\| isVideo\(displayedBackground\)[\s\S]*displayedBackground = finalPath/.test(backgroundQml),
'video switches bypass the image-only reveal stack and use the durable background path'
)
assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share still rendering')
assert(
!backgroundQml.includes('videoEnabled') &&
!backgroundQml.includes('displayedReloads') &&
!mediaQml.includes('displayedReloads'),
'the desktop hands video backgrounds to OWE with no player switch to carry'
)
assert(
!backgroundQml.includes('playbackEnabled') &&
!backgroundQml.includes('sessionObscured') &&
!backgroundQml.includes('fullscreenHere') &&
!backgroundQml.includes('Hyprland.monitorFor') &&
!backgroundQml.includes('omarchy.lock') &&
!backgroundQml.includes('omarchy.battery'),
'the desktop no longer carries the shell video pause policy'
)
assert(
barTextColor.includes('magick "$background_path[0]"'),
'bar colour sampling reads one frame instead of decoding a whole video'
)
assert(
menuImages.includes('pending_video_file') && /video_jobs=\$\(\( \$\(nproc\) \/ 4 \)\)/.test(menuImages),
'video thumbnails fan out narrower than single-threaded vips jobs'
)
assert(
themeSwitcher.includes('fast_signature="v2"'),
'the theme preview cache rebuilds after preview discovery learned about video'
)
assert(
/lazy_thumbnails == true && \$cache_only != true \]\] && ! is_video_path/.test(menuImages),
'a video never stands in as its own lazy thumbnail, so the fan out cap always applies'
)
assert(
/thumbnail_command=\(timeout -k \d+ \d+ ffmpegthumbnailer/.test(menuImages),
'a stalled video cannot hold the picker shut, because its generator is time bounded'
)
assert(
directImageList.includes('generate_video_thumbnail') &&
/timeout -k \d+ \d+ ffmpegthumbnailer/.test(directImageList) &&
/if \[\[ ! -f \$thumbnail \]\] && ! is_video_path/.test(directImageList),
'a direct picker scan generates bounded video thumbnails without content-hashing the media'
)
assert(
themeSet.includes('choose_staged_theme_background') &&
themeSet.includes('background_transition_uses_snapshots') &&
themeSet.includes('BACKGROUND_TRANSITION_SNAPSHOTS=false') &&
/if \[\[ \$BACKGROUND_TRANSITION_SNAPSHOTS == "true" \]\]/.test(themeSet) &&
/if \[\[ -z \$CHOSEN_THEME_BACKGROUND \|\| ! -f \$CHOSEN_THEME_BACKGROUND \]\]/.test(themeSet),
'theme changes disable both transition snapshots whenever either side is a video'
)
assert(
/function onScreensChanged\(\) \{[\s\S]*?root\.displaysBlank = false/.test(lockService),
'a display coming back gives up the blank state instead of freezing a visible wallpaper'
)
assert(
lockService.includes('function screenBlank(screenName)') &&
lockService.includes('function applyMonitorDpms(text)') &&
lockService.includes('command: ["hyprctl", "monitors", "-j"]') &&
lockService.includes('running: root.locked && root.videoBackground') &&
/displaysBlank: root\.screenBlank\(lockSurface\.screen/.test(lockService) &&
/function runWake\(\) \{[\s\S]*?root\.monitorDpmsKnown = false/.test(lockService) &&
/function runBlank\(\) \{[\s\S]*?root\.monitorDpmsKnown = false/.test(lockService),
'a locked video wallpaper follows what each panel actually did, not only what the lock asked for'
)
assert(
/feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml) &&
/displaysBlank: root\.screenBlank\(/.test(lockService) &&
/powerSaverActive: root\.powerSaverActive/.test(lockService) &&
/function runBlank\(\) \{\s*\n\s*root\.displaysBlank = true/.test(lockService) &&
/function runWake\(\) \{\s*\n\s*root\.displaysBlank = false/.test(lockService),
'the lock feed stops once displays go dark or power-saver is active'
)
assert(
batteryService.includes('property string activePowerProfile') &&
batteryService.includes('UPower.onBattery && activePowerProfile === "power-saver"') &&
batteryService.includes('"get-property", "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", "ActiveProfile"') &&
!batteryService.includes('["powerprofilesctl"') &&
batteryService.includes('interval: 2000'),
'the battery service tracks the active power-saver profile'
)
assert(
themeSwitcher.includes("-iname '*.mp4'") &&
themeSwitcher.includes('mp4 m4v mov webm mkv avi') &&
themeSwitcher.includes('preview.mp4'),
'theme switcher previews video-only themes, named preview files included'
)
assert(quattroUpgrade.includes("-iname '*.mp4'"), 'Quattro upgrade can seed a video-only theme background')
JS
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin" "$test_tmp/backgrounds" "$test_tmp/generator-cache" "$test_tmp/direct-cache"
printf 'not a real video\n' >"$test_tmp/backgrounds/sample.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
while (( $# > 0 )); do
case "$1" in
-i) input=$2; shift 2 ;;
-o) output=$2; shift 2 ;;
*) shift ;;
esac
done
printf 'thumbnail for %s\n' "$input" >"$output"
SH
chmod +x "$test_tmp/bin/ffmpegthumbnailer"
cat >"$test_tmp/bin/md5sum" <<'SH'
#!/bin/bash
if (( $# > 0 )); then
printf 'unexpected file hash: %s\n' "$*" >>"$MD5_FILE_CALLS"
fi
exec /usr/bin/md5sum "$@"
SH
chmod +x "$test_tmp/bin/md5sum"
md5_file_calls="$test_tmp/md5-file-calls"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/generator-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$test_tmp/backgrounds"
generator_thumbnail=$(find "$test_tmp/generator-cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.jpg' -print -quit)
[[ -s $generator_thumbnail ]] || fail "menu image generator creates a video thumbnail"
generator_row=$(XDG_CACHE_HOME="$test_tmp/generator-cache" "$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
IFS=$'\t' read -r generator_row_path generator_row_thumbnail <<<"$generator_row"
[[ $generator_row_path == "$test_tmp/backgrounds/sample.mp4" && $generator_row_thumbnail == "$generator_thumbnail" ]] || \
fail "direct picker consumes the menu image generator thumbnail" "$generator_row"
row=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/direct-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
IFS=$'\t' read -r row_path row_thumbnail <<<"$row"
[[ $row_path == "$test_tmp/backgrounds/sample.mp4" && $row_thumbnail == *.jpg && -s $row_thumbnail ]] || \
fail "image picker lists videos with their cached thumbnail" "$row"
[[ ! -s $md5_file_calls ]] || fail "direct video scans avoid hashing the complete media file" "$(<"$md5_file_calls")"
failed_backgrounds="$test_tmp/failed-backgrounds"
failed_cache="$test_tmp/failed-cache"
mkdir -p "$failed_backgrounds" "$failed_cache"
printf 'broken video\n' >"$failed_backgrounds/broken.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
exit 1
SH
cached_row=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$test_tmp/direct-cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$test_tmp/backgrounds")
[[ $cached_row == "$row" ]] || fail "direct picker reuses its cached video thumbnail" "$cached_row"
failed_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$failed_cache" MD5_FILE_CALLS="$md5_file_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$failed_backgrounds")
[[ -z $failed_rows ]] || fail "direct picker omits a video whose thumbnail fails" "$failed_rows"
failed_marker=$(find "$failed_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -n $failed_marker ]] || fail "direct picker remembers a video the converter rejected"
thumbnailer_calls="$test_tmp/thumbnailer-calls"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$THUMBNAILER_CALLS"
exit 1
SH
failed_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/shell/plugins/image-picker/list.sh" "$failed_backgrounds")
[[ -z $failed_rows && ! -e $thumbnailer_calls ]] || fail "direct picker skips a rejected video on the next scan" "$(cat "$thumbnailer_calls" 2>/dev/null)"
generator_failed_cache="$test_tmp/generator-failed-cache"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ -s $thumbnailer_calls ]] || fail "menu image generator tries a video it has not seen"
generator_marker=$(find "$generator_failed_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -n $generator_marker ]] || fail "menu image generator remembers a video the converter rejected"
rm -f "$thumbnailer_calls"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ ! -e $thumbnailer_calls ]] || fail "menu image generator skips a rejected video on the next open" "$(<"$thumbnailer_calls")"
# A repaired file gets a fresh key, so the old marker no longer applies, and
# the rows are not cached over its absence, so an in-place repair that leaves
# the directory's mtime alone is still noticed.
touch -d '2 minutes' "$failed_backgrounds/broken.mp4"
PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$generator_failed_cache" THUMBNAILER_CALLS="$thumbnailer_calls" \
"$ROOT/bin/omarchy-menu-images" --prepare-only "$failed_backgrounds"
[[ -s $thumbnailer_calls ]] || fail "menu image generator retries a video that changed since it was rejected"
timeout_backgrounds="$test_tmp/timeout-backgrounds"
timeout_cache="$test_tmp/timeout-cache"
mkdir -p "$timeout_backgrounds"
printf 'slow video\n' >"$timeout_backgrounds/slow.mp4"
cat >"$test_tmp/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
exit 124
SH
timeout_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$timeout_cache" \
"$ROOT/shell/plugins/image-picker/list.sh" "$timeout_backgrounds")
[[ -z $timeout_rows ]] || fail "direct picker omits a video whose thumbnail timed out" "$timeout_rows"
timeout_marker=$(find "$timeout_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -z $timeout_marker ]] || fail "a timed out video is left to retry rather than remembered as failed"
grep -qx 'owe' "$ROOT/install/omarchy-base.packages" || fail "OWE is a base package"
grep -qx 'owe-lockfeed' "$ROOT/install/omarchy-base.packages" || fail "the OWE lock feed module is a base package"
if grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages"; then
fail "Qt Multimedia is no longer needed by the shell"
fi
pass "menu image generator creates thumbnails consumed by the picker"
pass "direct picker generates and reuses still thumbnails"
pass "direct picker omits videos whose thumbnails cannot be generated"
pass "a rejected video is remembered so it costs nothing on the next open"
pass "a timed out video is left to retry"
pass "a locked video wallpaper follows the panels' real DPMS state"
pass "OWE and its lock feed module are declared"
source <(awk '
/^(is_video_path|snapshot_background_path|background_transition_uses_snapshots|choose_theme_background|choose_staged_theme_background|set_theme_background)\(\) \{/ { copying=1 }
copying { print }
copying && /^}$/ { copying=0 }
' "$ROOT/bin/omarchy-theme-set")
transition_home="$test_tmp/transition-home"
CURRENT_THEME_PATH="$transition_home/.local/state/omarchy/current/theme"
NEXT_THEME_PATH="$transition_home/.local/state/omarchy/current/next-theme"
CURRENT_BACKGROUND_LINK="$transition_home/.local/state/omarchy/current/background"
BACKGROUND_TRANSITION_CACHE="$transition_home/.cache/omarchy/background-transitions"
THEME_NAME="video-test"
HOME="$transition_home"
mkdir -p "$CURRENT_THEME_PATH/backgrounds" "$NEXT_THEME_PATH/backgrounds" "$HOME/.config/omarchy/backgrounds/$THEME_NAME"
printf 'old image\n' >"$CURRENT_THEME_PATH/backgrounds/old.png"
printf 'old image staged\n' >"$NEXT_THEME_PATH/backgrounds/old.png"
printf 'new video\n' >"$NEXT_THEME_PATH/backgrounds/new.mp4"
ln -s "$CURRENT_THEME_PATH/backgrounds/old.png" "$CURRENT_BACKGROUND_LINK"
choose_staged_theme_background || fail "staged video background is selected before the theme swap"
expected_staged_background="$CURRENT_THEME_PATH/backgrounds/new.mp4"
[[ $CHOSEN_THEME_BACKGROUND == $expected_staged_background ]] || \
fail "staged background resolves to its durable post-swap path" "$CHOSEN_THEME_BACKGROUND"
if background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
fail "image to video theme transitions skip snapshots"
fi
background_transition_uses_snapshots "$CURRENT_THEME_PATH/backgrounds/new.png" || \
fail "image to image theme transitions retain snapshots"
rm "$CURRENT_BACKGROUND_LINK"
printf 'old video\n' >"$CURRENT_THEME_PATH/backgrounds/old.mp4"
ln -s "$CURRENT_THEME_PATH/backgrounds/old.mp4" "$CURRENT_BACKGROUND_LINK"
if background_transition_uses_snapshots "$CURRENT_THEME_PATH/backgrounds/new.png"; then
fail "video to image theme transitions skip snapshots"
fi
video_snapshot=$(snapshot_background_path "$CURRENT_THEME_PATH/backgrounds/old.mp4" "video")
[[ -z $video_snapshot && ! -e $BACKGROUND_TRANSITION_CACHE ]] || fail "video files are never snapshotted"
CHOSEN_THEME_BACKGROUND="$transition_home/disappeared.mp4"
BACKGROUND_TRANSITION_SNAPSHOTS=false
OLD_BACKGROUND_SNAPSHOT=""
colors_payload=""
shell_payload=""
shell_ipc() { :; }
set_theme_background
[[ -f $CHOSEN_THEME_BACKGROUND && $(readlink "$CURRENT_BACKGROUND_LINK") == "$CHOSEN_THEME_BACKGROUND" ]] || \
fail "theme changes recover when a preselected background disappears" "$CHOSEN_THEME_BACKGROUND"
pass "theme transitions skip snapshots whenever either side is a video"
pass "theme changes recover from a missing preselected background"