test(nix): share the wallpaper scenario, and the files kfw must sync

Wallpapers and the colors of the borders that come from them are in the
config of both ports. wallpaper.run is the neutral part: a monitor's
wallpaper at start, a workspace's own while it is shown, the ring in the
hue of the wallpaper, and a changed wallpaper put back. Platform gets
put_file, wallpaper and set_wallpaper; in a Plasma session restart_with
restarts the module's unit with the user's komorebi.json. The KDE extra
keeps what only the shell tells. sync-e2e-core carries the two fakes the
shared tests import.
This commit is contained in:
LGUG2Z committed 2026-10-30 16:18:32 -07:00
1 parent a8e79ca5d2
commit dedcab71bc
14 files changed
+374 -141

No files matched your search

+1
View File
@@ -61,6 +61,7 @@ runs, and goes stale when the code changes.
| `tiling_off` | `toggle-tiling` leaves the windows where they are, a layout change moves none, and tiling again tiles them | pass | pass |
| `scrolling` | in the scrolling layout each window in turn has the focus and is inside the work area | pass | pass |
| `scrolling.columns` | five windows in two columns: the ones out of view rest at an edge with their width, the strip follows the focus and `pan`, and `consume`, `expel`, `toggle-column-stack` and `scrolling-layout-full-width` work | pass | not run yet |
| `wallpaper` | a monitor's wallpaper at start, a workspace's own while shown, the ring in the hue of the wallpaper, and a changed wallpaper put back | pass 12 | not run yet |
| `scrolling.two_monitors` | a strip with a display next to it: scrolling with `focus` and `pan` shows no window on the second display | pass | not run yet |
| `monitors` | the monitor commands (`send-to-monitor`, `move-to-monitor`, their cycle, workspace and named forms, `focus-monitor`, `cycle-monitor`, `focus-monitor-workspace`, `cycle-move-workspace-to-monitor`, `focus-monitor-at-cursor` where there is a pointer) put each window on the monitor and workspace asked for, tiled in its display, and the focus where each leaves it | not run | not run |
| `layout_flip` | `layout_flip` in the config (`Horizontal`, `Vertical`, `HorizontalAndVertical`) mirrors a workspace's layout from the start, `flip-layout` turns it off and on, and it is the named workspace's own | not run | not run |
+1
View File
@@ -23,6 +23,7 @@ docstrings of `Platform` are the contract; this is the audit behind them.
| `displays()` | each display's rectangle in the desktop's coordinates, left to right, as the system says | displays are not one size (on Windows the first is the basic adapter, 1280x800, and the second, the only one whose mode can change, is 1024x768 by default and follows `resize_display`) and a scenario does not compute where the next begins |
| `add_display()`, `remove_display()` | plugs a display in to the right, unplugs the last; return when the system agrees | Windows removes only a display `add_display` plugged in (a hotplug port is used once); KDE enables a disabled output and disables the last enabled one, so a headless session with two outputs can unplug at once. A scenario unplugs only after plugging in, unless its suite started with more than one display |
| `work_area()` | the area of the first display that windows may take, as the system says: the display less panels, docks and the taskbar | KDE asks KWin (a script: its placement area), and answers with the whole output only if KWin does not answer. A headless session has no panel, so there it is the output |
| `put_file(path, data)`, `wallpaper(display)`, `set_wallpaper(path, display)` | a file the daemon can read; the picture a display shows; the wallpaper as the user sets it | KDE reads and sets it through the Plasma shell's D-Bus API. Windows: `IDesktopWallpaper`. `restart_with` in a real Plasma session restarts the module's unit with the user's `komorebi.json` |
| `resize_grab(frame, grab)` | where the pointer is pressed to take a window by an edge or corner | KDE: the gap outside the frame on every side (`resize.outside_grab`). Windows: left, right and bottom are the invisible border outside the visible frame; the top has none, so it is inside the frame's top row |
| `close`, `minimize`, `activate`, `move_window` | act on the window as the user would; raise if there is no such window | `close` returns before the window is gone (a scenario waits for `os_window` to be `None`); `move_window` puts the top left of the *visible* frame at (x, y) |
| `lock()`, `unlock()` | lock and unlock the screen and return when it is so | the lock screen is `LogonUI` on Windows and the Plasma greeter on KDE; a scenario asks only what the daemon and the windows do |
+11 -1
View File
@@ -226,6 +226,16 @@ Not kept: for features A, "komorebi still has all three" (the shared scenario ch
it is a check of the window it moves). The shared `fullscreen` is looser than E in one way: the
window covers the display up to a pixel, where E compared the frame with the monitor exactly.
## wallpaper
Wallpapers and the colors of the borders that come from them are in the config of both ports, so
the shared scenario `wallpaper.run` has what is neutral: a monitor's wallpaper at start, a
workspace's own while it is shown (a path with a space in it), the ring of the focused window in
the hue of the wallpaper, and a wallpaper the user changed being put back. It uses `put_file`,
`wallpaper` and `set_wallpaper`. The extra `wallpaper.run` keeps what only the Plasma shell tells:
the fill mode and how long a switch takes, a switch by a pager, the palette cache, and a daemon
that starts before the shell. 12 checks went to the shared scenario, 12 stayed.
## The KDE-only suites
Moved as they were into `komorebi_vm.platforms.kde.extras`, one module per suite (`kwin`,
@@ -256,7 +266,7 @@ move:
| module | 33 |
| live-resize | 7 |
| look | 40 |
| wallpaper | 22 |
| wallpaper | 12 |
| features | 30 |
| keys-engine | 34 |
| run-kde | 22 |
@@ -258,6 +258,20 @@ class Platform(Protocol):
size."""
...
def put_file(self, path: str, data: bytes) -> None:
"""Writes `data` to `path` on the machine, where the daemon can read it, and makes the
directories above it. `path` is one `temp_path` made."""
...
def wallpaper(self, display: int = 0) -> str:
"""The path of the picture a display shows, as the operating system says it (the Plasma
shell on KDE, `IDesktopWallpaper` on Windows), or the empty string when it does not know."""
...
def set_wallpaper(self, path: str, display: int = 0) -> None:
"""Sets the wallpaper of a display as the user does, and returns when it is set."""
...
def resize_grab(self, frame: Rect, grab: str) -> tuple[int, int]:
"""Where the pointer is pressed to take a window of this frame by `grab`: `left`, `right`,
`top`, `bottom`, or a corner (`topleft`, `topright`, `bottomleft`, `bottomright`).
@@ -0,0 +1,160 @@
"""Wallpapers, and the colors of the borders that come from them.
A monitor's wallpaper is set when the daemon starts, and the wallpaper of a workspace replaces it
while the workspace is shown. The borders of the windows take the colors of the wallpaper's
palette, and follow it from workspace to workspace. A wallpaper the user changed by hand is put back
when its workspace is shown again.
The pictures are a dozen bands of color each, since a palette wants at least eight: a warm one for
the monitor, a cool one for the second workspace, and a green one the user sets by hand. Both ports
read the same `wallpaper` of `komorebi.json`, and keep a palette for each picture.
"""
import io
from PIL import Image
from komorebi_vm.core import pixels
from komorebi_vm.core.checks import Checks
from komorebi_vm.core.geometry import Rect
from komorebi_vm.core.platform import Client, Platform
def picture(hue: tuple[int, int, int], ramp: int) -> bytes:
"""A 192 by 108 PNG of diagonal bands of `hue`, going dark and light, `ramp` varying the hue."""
data: list[tuple[int, int, int]] = []
for y in range(108):
for x in range(192):
band = (x // 16 + y // 16) % 12
shade = 40 + (band % 4) * 55
red, green, blue = hue
green = min(255, green + band * ramp)
data.append(
(min(255, red * shade // 255), min(255, green * shade // 255), blue * shade // 255)
)
image = Image.new("RGB", (192, 108))
image.putdata(data)
out = io.BytesIO()
image.save(out, "PNG")
return out.getvalue()
def _hue(red: int, blue: int) -> str:
return "warm" if red > blue + 40 else "cool" if blue > red + 40 else "neither"
def hue_of(image: Image.Image) -> str:
"""Warm, cool or neither: the average color of a grid of points of the screen."""
width, height = image.size
points = [
pixels.pixel(image, width * i // 10, height * j // 12)
for i in range(1, 10)
for j in range(1, 9)
]
return _hue(
round(sum(p[0] for p in points) / len(points)),
round(sum(p[2] for p in points) / len(points)),
)
def ring_hue(frame: Rect, image: Image.Image, reach: int = 12) -> str:
"""The hue of the ring around a window: the colors within `reach` pixels of the left edge of
its frame, on either side of it, at its middle. The window's own color is neutral, so what has
a hue is the border; where the ports draw it, outside the frame or on its edge, differs."""
seen = set()
for x in range(max(0, frame.x - reach), min(image.size[0], frame.x + reach)):
red, _, blue = pixels.pixel(image, x, frame.y + frame.h // 2)
seen.add(_hue(red, blue))
seen.discard("neither")
return seen.pop() if len(seen) == 1 else "neither" if not seen else "mixed"
def run(platform: Platform, checks: Checks) -> None:
"""The daemon starts with a wallpaper for the monitor and one for its second workspace; the
scenario follows them from workspace to workspace, with the borders, and sets one by hand.
The workspace is empty, and so it is when the scenario ends."""
warm = platform.temp_path("wallpapers/warm.png")
cool = platform.temp_path("wallpapers/my pictures/cool.png")
other = platform.temp_path("wallpapers/other.png")
platform.put_file(warm, picture((255, 0, 20), 20))
platform.put_file(cool, picture((20, 80, 255), 14))
platform.put_file(other, picture((60, 255, 60), 0))
def shows(path: str) -> bool:
return platform.wallpaper() == path
def screen_is(hue: str, name: str) -> bool:
"""Takes pictures of the screen until it is `hue`: one can catch the shell half way."""
return platform.wait_for(lambda: hue_of(platform.screenshot(name)) == hue, timeout=20)
def has_ring(client: Client, hue: str, name: str) -> bool:
"""The window has a ring of `hue` in a screenshot taken until it does."""
def ring() -> bool:
window = platform.os_window(client)
return window is not None and ring_hue(window.frame, platform.screenshot(name)) == hue
return platform.wait_for(ring, timeout=20)
platform.set_wallpaper(other)
checks.check(
"the screen shows a picture of the user's choice to begin with",
platform.wait_for(lambda: shows(other), timeout=20),
)
platform.restart_with(
{
"monitors": [
{
"wallpaper": {"path": warm},
"workspaces": [
{"name": "one"},
{"name": "two", "wallpaper": {"path": cool}},
],
}
]
}
)
checks.check(
"at start the screen shows the monitor's wallpaper",
platform.wait_for(lambda: shows(warm), timeout=20),
)
checks.check("and the screen is warm", screen_is("warm", "wallpaper-a1"))
platform.komorebic("focus-workspace", "1")
checks.check(
"workspace two's own wallpaper is shown when it is (the path has a space in it)",
platform.wait_for(lambda: shows(cool), timeout=10),
)
checks.check("and the screen is cool", screen_is("cool", "wallpaper-a2"))
platform.komorebic("focus-workspace", "0")
checks.check(
"back on workspace one, the monitor's wallpaper again",
platform.wait_for(lambda: shows(warm), timeout=10),
)
left = platform.spawn("wallpaper-left")
checks.check(
"on the warm workspace the focused window's ring is warm",
has_ring(left, "warm", "wallpaper-c1"),
)
platform.komorebic("focus-workspace", "1")
right = platform.spawn("wallpaper-right")
checks.check("on the cool one it is cool", has_ring(right, "cool", "wallpaper-c2"))
platform.komorebic("focus-workspace", "0")
checks.check("and warm again when the first is back", has_ring(left, "warm", "wallpaper-c3"))
platform.set_wallpaper(other)
checks.check("the user changed it", platform.wait_for(lambda: shows(other), timeout=10))
platform.komorebic("focus-workspace", "1")
platform.wait_for(lambda: shows(cool), timeout=10)
platform.komorebic("focus-workspace", "0")
checks.check(
"showing the workspace again set it back",
platform.wait_for(lambda: shows(warm), timeout=10),
)
platform.close(left)
platform.komorebic("focus-workspace", "1")
platform.close(right)
platform.komorebic("focus-workspace", "0")
@@ -1,10 +1,12 @@
"""KDE as a [`Platform`](komorebi_vm.core.platform.Platform): a `Kde` session, seen through the
neutral vocabulary."""
import base64
import json
import re
import shlex
from typing import Any
from urllib.parse import unquote
from PIL import Image
@@ -75,7 +77,24 @@ class KdePlatform:
def crash(self) -> None:
self.kde.kill_komorebi()
def _restart_the_modules_daemon(self, config: dict[str, Any]) -> None:
"""A real Plasma session has the module's unit run the daemon, with the user's own
`komorebi.json`. The unit `komorebi@session` of a headless run has a bus of its own,
and a daemon on it has no Plasma shell to talk to."""
kde = self.kde
kde.systemctl("stop", "komorebi.service")
kde.wait_for(lambda: kde.run("komorebic state")[0] != 0, timeout=30)
kde.close_colored_windows()
kde.sh("mkdir -p ~/.config/komorebi")
kde.sh(f"printf %s {shlex.quote(json.dumps(config))} > ~/.config/komorebi/komorebi.json")
kde.systemctl("start", "komorebi.service")
if not kde.wait_for(lambda: kde.run("komorebic state")[0] == 0, timeout=60):
raise AssertionError("the module's daemon did not answer komorebic")
def restart_with(self, config: dict[str, Any]) -> None:
if self.session:
self._restart_the_modules_daemon(config)
return
if self.kde.komorebi_running():
self.kde.komorebic("stop")
self.kde.wait_for(lambda: not self.kde.komorebi_running(), timeout=30)
@@ -227,6 +246,24 @@ class KdePlatform:
if not self.kde.wait_for(lambda: self._locked(False), timeout=30):
raise AssertionError("the screen did not unlock")
def put_file(self, path: str, data: bytes) -> None:
directory = shlex.quote(path.rsplit("/", 1)[0])
encoded = base64.b64encode(data).decode()
self.kde.sh(f"mkdir -p {directory} && chmod a+rx {directory}")
self.kde.sh(
f"echo {encoded} | base64 -d > {shlex.quote(path)} && chmod a+r {shlex.quote(path)}"
)
def wallpaper(self, display: int = 0) -> str:
shell = "org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell"
out = self.kde.run(f"busctl --user call {shell} wallpaper u {display}")[1]
image = re.search(r'"Image" s "([^"]*)"', out)
url = image.group(1) if image else ""
return unquote(url.removeprefix("file://"))
def set_wallpaper(self, path: str, display: int = 0) -> None:
self.kde.sh(f"plasma-apply-wallpaperimage {shlex.quote(path)}")
def resize_grab(self, frame: Rect, grab: str) -> tuple[int, int]:
return outside_grab(frame, grab)
@@ -1,28 +1,23 @@
r"""Wallpapers, and the borders' colors that come from them, in a real Plasma session: what the shell
shows and what is on the screen, read from the shell's D-Bus API and from screenshots.
r"""What only the Plasma shell tells about wallpapers, beside the shared `wallpaper` scenario, which
sets them, follows them from workspace to workspace with the borders, and sets one back: how the
shell fills the screen, a switch that komorebi did not make, the palette cache and a shell that
starts late.
A a monitor's wallpaper is set at start, and a workspace's own replaces it when it is shown
A the fill mode of the wallpaper, and how long a workspace takes to change it
B a switch komorebi did not make (a pager's) sets the wallpaper too
C the borders take the colors of the wallpaper's palette, and follow it from workspace to
workspace
D a wallpaper the user changed by hand is put back when its workspace is shown again
E the palette cache, and a path with a space in it (the second picture's)
E the palette cache
F komorebi that starts before the Plasma shell does
The pictures are a dozen bands of color each, since a palette wants at least eight: a warm one for
the monitor, a cool one for the second workspace, and a green one the test sets by hand.
"""
import base64
import io
import re
import shlex
import time
from typing import Any
from PIL import Image
from komorebi_vm import Checks, Kde, Rect, pixels
from komorebi_vm import Checks, Kde
UNIT = "komorebi.service"
SHELL_UNIT = "plasma-plasmashell.service"
@@ -44,36 +39,6 @@ def run(machine: Any, checks: Checks, scale: float) -> None:
SCALE = scale # noqa: N806
kde = Kde.session(machine, scale=SCALE)
def picture(hue: tuple[int, int, int], ramp: int) -> bytes:
"""A 192 by 108 PNG of diagonal bands of `hue`, going dark and light, `ramp` varying the hue."""
data: list[tuple[int, int, int]] = []
for y in range(108):
for x in range(192):
band = (x // 16 + y // 16) % 12
shade = 40 + (band % 4) * 55
red, green, blue = hue
green = min(255, green + band * ramp)
data.append(
(
min(255, red * shade // 255),
min(255, green * shade // 255),
blue * shade // 255,
)
)
image = Image.new("RGB", (192, 108))
image.putdata(data)
out = io.BytesIO()
image.save(out, "PNG")
return out.getvalue()
def put(path: str, data: bytes) -> None:
"""Writes `data` to `path` in the machine, where the test user can read it."""
directory = shlex.quote(path.rsplit("/", 1)[0])
encoded = base64.b64encode(data).decode()
machine.succeed(f"mkdir -p {directory} && chmod a+rx {directory}")
machine.succeed(f"echo {encoded} | base64 -d > {shlex.quote(path)}")
machine.succeed(f"chmod a+r {shlex.quote(path)}")
def running() -> bool:
return kde.run(f"systemctl --user is-active --quiet {UNIT}")[0] == 0
@@ -101,78 +66,30 @@ def run(machine: Any, checks: Checks, scale: float) -> None:
def shows(url: str) -> bool:
return shell_wallpaper()[0] == url
def hue_of(image: Image.Image) -> str:
"""Warm, cool or neither: the average color of a grid of points of the screen."""
width, height = image.size
points = [
pixels.pixel(image, width * i // 10, height * j // 12)
for i in range(1, 10)
for j in range(1, 9)
]
red = sum(p[0] for p in points) / len(points)
blue = sum(p[2] for p in points) / len(points)
return "warm" if red > blue + 40 else "cool" if blue > red + 40 else "neither"
def screen_is(hue: str, name: str) -> bool:
"""Takes pictures of the screen until it is `hue`: one can catch the shell half way."""
return kde.wait_for(lambda: hue_of(kde.shot(name)) == hue, timeout=20)
def ring_hue(frame: Rect, image: Image.Image) -> str:
"""The hue of the ring just outside the left edge of a frame."""
red, _, blue = pixels.pixel(image, frame.x - 5, frame.y + frame.h // 2)
return "warm" if red > blue + 40 else "cool" if blue > red + 40 else "neither"
def has_ring(title: str, hue: str, name: str) -> bool:
"""Returns true if the window has a ring of `hue` in a screenshot taken until it does."""
return kde.wait_for(
lambda: ring_hue(kde.rect(kde.wid(title)), kde.shot(name)) == hue, timeout=20
)
print("== the pictures, and the wallpapers of the configuration")
put(WARM, picture((255, 0, 20), 20))
put(COOL, picture((20, 80, 255), 14))
put(OTHER, picture((60, 255, 60), 0))
kde.sh(f"mkdir -p {shlex.quote(CONFIG.rsplit('/', 1)[0])}")
# the shared `wallpaper` scenario ran before this: the daemon runs with the wallpapers of its
# configuration, the pictures are in PICTURES, and the first workspace shows the warm one
# the shared scenario restarted the module's unit with the user's `komorebi.json`
# the unit starts komorebi with the user's own `komorebi.json`: the wallpapers are in it too
config = (
'{ "monitors": [ { "wallpaper": { "path": "' + WARM + '" },'
' "workspaces": [ { "name": "one" },'
' { "name": "two", "wallpaper": { "path": "' + COOL + '" } } ] } ] }'
)
kde.sh(f"mkdir -p {shlex.quote(CONFIG.rsplit('/', 1)[0])}")
kde.sh(f"echo {shlex.quote(config)} > {CONFIG}")
kde.sh("rm -rf ~/.cache/komorebi")
# the session started the module's unit when it logged in: this test starts its own runs
stop_komorebi()
kde.sh(f"plasma-apply-wallpaperimage {shlex.quote(OTHER)}")
checks.check(
"the shell shows a picture of the user's choice to begin with",
kde.wait_for(lambda: shows(f"file://{OTHER}"), timeout=20),
)
print("== A. set at start, and by workspace")
start_komorebi()
checks.check(
"at start the shell shows the monitor's wallpaper",
kde.wait_for(lambda: shows(WARM_URL), timeout=20),
)
checks.check("it fills the screen", shell_wallpaper()[1] == FILL)
checks.check("and the screen is warm", screen_is("warm", "a1"))
print("== A. what only the Plasma shell tells")
checks.check("the monitor's wallpaper fills the screen", shell_wallpaper()[1] == FILL)
began = time.monotonic()
kde.komorebic("focus-workspace", "1")
checks.check(
"workspace two's own wallpaper is shown when it is (the path has a space in it)",
"workspace two's own wallpaper is shown when it is",
kde.wait_for(lambda: shows(COOL_URL), timeout=10),
)
took = time.monotonic() - began
checks.check(f"it took under two seconds ({took:.1f} s)", took < 2 * max(1, SCALE))
checks.check("and the screen is cool", screen_is("cool", "a2"))
kde.komorebic("focus-workspace", "0")
checks.check(
"back on workspace one, the monitor's wallpaper again",
kde.wait_for(lambda: shows(WARM_URL), timeout=10),
)
kde.wait_for(lambda: shows(WARM_URL), timeout=10)
print("== B. a switch somebody else made")
kde.sh("kdotool set_desktop 2")
@@ -183,35 +100,6 @@ def run(machine: Any, checks: Checks, scale: float) -> None:
kde.sh("kdotool set_desktop 1")
checks.check("and back", kde.wait_for(lambda: shows(WARM_URL), timeout=10))
print("== C. the borders follow the wallpaper")
kde.colored_window("left", "303030")
time.sleep(1.5)
checks.check(
"on the warm workspace the focused window's ring is warm", has_ring("left", "warm", "c1")
)
kde.komorebic("focus-workspace", "1")
time.sleep(2)
kde.colored_window("right", "303030")
time.sleep(2)
checks.check("on the cool one it is cool", has_ring("right", "cool", "c2"))
kde.komorebic("focus-workspace", "0")
time.sleep(2.5)
checks.check("and warm again when the first is back", has_ring("left", "warm", "c3"))
print("== D. a wallpaper changed by hand is put back")
kde.sh(f"plasma-apply-wallpaperimage {shlex.quote(OTHER)}")
checks.check(
"the user changed it",
kde.wait_for(lambda: shows(f"file://{OTHER}"), timeout=10),
)
kde.komorebic("focus-workspace", "1")
time.sleep(1)
kde.komorebic("focus-workspace", "0")
checks.check(
"showing the workspace again set it back",
kde.wait_for(lambda: shows(WARM_URL), timeout=10),
)
print("== E. the palette is kept")
cache = kde.sh("ls ~/.cache/komorebi 2>/dev/null || true").split()
print(f" the cache: {cache}")
+9
View File
@@ -157,6 +157,15 @@ class FakeDesk:
def unlock(self) -> None:
raise NotImplementedError
def put_file(self, path: str, data: bytes) -> None: # noqa: ARG002
pass
def wallpaper(self, display: int = 0) -> str: # noqa: ARG002
return ""
def set_wallpaper(self, path: str, display: int = 0) -> None:
pass
def resize_grab(self, frame, grab): # noqa: ANN001, ANN201
return outside_grab(frame, grab)
+9
View File
@@ -215,6 +215,15 @@ class FakePlatform:
if self.retiles_on_unlock:
self.moved.clear()
def put_file(self, path: str, data: bytes) -> None: # noqa: ARG002
pass
def wallpaper(self, display: int = 0) -> str: # noqa: ARG002
return ""
def set_wallpaper(self, path: str, display: int = 0) -> None:
pass
def resize_grab(self, frame, grab): # noqa: ANN001, ANN201
return outside_grab(frame, grab)
+64
View File
@@ -0,0 +1,64 @@
"""A fake that sets wallpapers from the configuration it is started with, shows them on its
screenshots, and draws a ring around the focused window in a hue of the wallpaper."""
from typing import Any
from PIL import Image, ImageDraw
from komorebi_vm.core.platform import Client
from tests.fake_platform import FakePlatform
HUES = {"warm": (230, 40, 40), "cool": (40, 60, 230), "other": (60, 230, 60)}
# the ring is the 4 pixels of the wallpaper's hue that the window leaves at its left
class FakeWallpaper(FakePlatform):
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
super().__init__(*args, **kwargs)
self.shown = ""
self.monitor_wallpaper = ""
self.workspace_wallpapers: dict[int, str] = {}
self.workspace = 0
self.puts_back = True
def put_file(self, path: str, data: bytes) -> None:
assert data.startswith(b"\x89PNG"), "a picture"
def wallpaper(self, display: int = 0) -> str:
return self.shown
def set_wallpaper(self, path: str, display: int = 0) -> None:
self.shown = path
def restart_with(self, config: dict[str, Any]) -> None:
self.running = True
monitor = config["monitors"][0]
self.monitor_wallpaper = monitor["wallpaper"]["path"]
self.workspace_wallpapers = {
i: w["wallpaper"]["path"]
for i, w in enumerate(monitor["workspaces"])
if "wallpaper" in w
}
self._show()
def _show(self) -> None:
self.shown = self.workspace_wallpapers.get(self.workspace, self.monitor_wallpaper)
def komorebic(self, *args: str) -> str:
if args[0] == "focus-workspace":
self.workspace = int(args[1])
if self.puts_back or self.shown != self.wallpaper():
self._show()
return ""
return super().komorebic(*args)
def screenshot(self, name: str) -> Image.Image:
which = "other" if "other" in self.shown else "cool" if "cool" in self.shown else "warm"
image = Image.new("RGB", (1920, 1080), HUES[which])
draw = ImageDraw.Draw(image)
for client in (Client(t) for c in self.containers for t in c):
window = self.os_window(client)
if window is not None and window.focused:
f = window.frame
draw.rectangle((f.x + 4, f.y, f.x + f.w, f.y + f.h), fill=(48, 48, 48))
return image
@@ -36,11 +36,13 @@ from komorebi_vm.core.scenarios import (
state_fidelity,
tiling,
tiling_off,
wallpaper,
workspace_moves,
workspaces,
)
from tests.fake_platform import FakePlatform
from tests.fake_strip import FakeStrip
from tests.fake_wallpaper import FakeWallpaper
def opened(platform: FakePlatform, checks: Checks): # noqa: ANN201
@@ -955,3 +957,19 @@ def test_a_daemon_that_rests_windows_on_the_next_monitor_fails_two_monitors() ->
checks = Checks()
scrolling.two_monitors(platform, checks, [])
assert checks.failed > 0
def test_a_daemon_that_sets_the_wallpapers_passes_wallpaper() -> None:
platform = FakeWallpaper()
checks = Checks()
wallpaper.run(platform, checks)
assert checks.failed == 0, checks.failures
assert checks.passed > 8
def test_a_daemon_that_leaves_a_changed_wallpaper_fails_wallpaper() -> None:
platform = FakeWallpaper()
platform.puts_back = False
checks = Checks()
wallpaper.run(platform, checks)
assert checks.failed > 0
+9
View File
@@ -104,6 +104,15 @@ class FakePlatform:
def unlock(self) -> None:
pass
def put_file(self, path: str, data: bytes) -> None: # noqa: ARG002
pass
def wallpaper(self, display: int = 0) -> str: # noqa: ARG002
return ""
def set_wallpaper(self, path: str, display: int = 0) -> None:
pass
def resize_grab(self, frame: Rect, grab: str) -> tuple[int, int]:
return outside_grab(frame, grab)
+24 -11
View File
@@ -1,25 +1,38 @@
# Wallpapers, and the borders' colors that come from them, in a real Plasma session: what the shell
# shows and what is on the screen, read from the shell's D-Bus API and from screenshots.
#
# A a monitor's wallpaper is set at start, and a workspace's own replaces it when it is shown
# B a switch komorebi did not make (a pager's) sets the wallpaper too
# C the borders take the colors of the wallpaper's palette, and follow it from workspace to
# workspace
# D a wallpaper the user changed by hand is put back when its workspace is shown again
# E the palette cache, and a path with a space in it (the second picture's)
# F komorebi that starts before the Plasma shell does
# The shared scenario `wallpaper` (`komorebi_vm.core.scenarios.wallpaper`, which runs on Windows
# too) sets a wallpaper for a monitor and one for a workspace, follows them from workspace to
# workspace with the colors of the borders, and sets one back that the user changed. The extra
# is what only the Plasma shell tells:
#
# The pictures are a dozen bands of color each, since a palette wants at least eight: a warm one for
# the monitor, a cool one for the second workspace, and a green one the test sets by hand.
# A the fill mode of the wallpaper, and how long a workspace takes to change it
# B a switch komorebi did not make (a pager's) sets the wallpaper too
# E the palette cache
# F komorebi that starts before the Plasma shell does
from komorebi_vm import Checks
from komorebi_vm.core.suite import platform_only, run_suite
from komorebi_vm import Kde
from komorebi_vm.core.scenarios import wallpaper
from komorebi_vm.core.suite import platform_only, run_suite, shared
from komorebi_vm.kde_platform import KdePlatform
from komorebi_vm.platforms.kde.extras import wallpaper as kde_wallpaper
start_all()
machine.wait_for_unit("multi-user.target")
checks = Checks()
kde = Kde.session(machine, scale=SCALE)
# the session started the module's unit when it logged in: the scenario restarts that daemon with
# the wallpapers it writes to the user's `komorebi.json`
checks.check("komorebi answers", kde.wait_for(lambda: kde.run("komorebic state")[0] == 0, 120))
kde.wait_for_panel_room()
size = kde.state()["monitors"]["elements"][0]["size"]
platform = KdePlatform(kde, (size["right"], size["bottom"]), session=True)
run_suite(checks, platform_only("kde", kde_wallpaper.run, machine, checks, SCALE))
run_suite(
checks,
shared(wallpaper.run, platform, checks),
platform_only("kde", kde_wallpaper.run, machine, checks, SCALE),
)
checks.finish()
+1 -1
View File
@@ -20,7 +20,7 @@ dest=${1:?usage: sync-e2e-core [--check] DEST}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
lib=$repo/nix/tests/vm/lib
tests=(fake_platform.py test_checks.py test_suite.py test_pixels.py test_snapshot.py test_recorded.py test_tiling.py
tests=(fake_platform.py fake_strip.py fake_wallpaper.py test_checks.py test_suite.py test_pixels.py test_snapshot.py test_recorded.py test_tiling.py
test_focus_moving.py)
status=0