1let
2 makeTest = import ./make-test-python.nix;
3 # Just to make sure everything is the same, need it for OCR & navigating greeter
4 user = "alice";
5 description = "Alice Foobar";
6 password = "foobar";
7
8 wallpaperName = "wallpaper.jpg";
9 # In case it ever shows up in the VM, we could OCR for it instead
10 wallpaperText = "Lorem ipsum";
11
12 # tmpfiles setup to make OCRing on terminal output more reliable
13 terminalOcrTmpfilesSetup =
14 {
15 pkgs,
16 lib,
17 config,
18 }:
19 let
20 white = "255, 255, 255";
21 black = "0, 0, 0";
22 colorSection = color: {
23 Color = color;
24 Bold = true;
25 Transparency = false;
26 };
27 terminalColors = pkgs.writeText "customized.colorscheme" (
28 lib.generators.toINI { } {
29 Background = colorSection white;
30 Foreground = colorSection black;
31 Color2 = colorSection black;
32 Color2Intense = colorSection black;
33 }
34 );
35 terminalConfig = pkgs.writeText "terminal.ubports.conf" (
36 lib.generators.toINI { } {
37 General = {
38 colorScheme = "customized";
39 fontSize = "16";
40 fontStyle = "Inconsolata";
41 };
42 }
43 );
44 confBase = "${config.users.users.${user}.home}/.config";
45 userDirArgs = {
46 mode = "0700";
47 user = user;
48 group = "users";
49 };
50 in
51 {
52 "${confBase}".d = userDirArgs;
53 "${confBase}/terminal.ubports".d = userDirArgs;
54 "${confBase}/terminal.ubports/customized.colorscheme".L.argument = "${terminalColors}";
55 "${confBase}/terminal.ubports/terminal.ubports.conf".L.argument = "${terminalConfig}";
56 };
57
58 wallpaperFile =
59 pkgs:
60 pkgs.runCommand wallpaperName
61 {
62 nativeBuildInputs = with pkgs; [
63 (imagemagick.override { ghostscriptSupport = true; }) # produce OCR-able image
64 ];
65 }
66 ''
67 magick -size 640x480 canvas:black -pointsize 30 -fill white -annotate +100+100 '${wallpaperText}' $out
68 '';
69
70 lomiriWallpaperDconfSettings = pkgs: {
71 settings = {
72 "org/gnome/desktop/background" = {
73 picture-uri = "file://${wallpaperFile pkgs}";
74 };
75 };
76 };
77
78 sharedTestFunctions = ''
79 def wait_for_text(text):
80 """
81 Wait for on-screen text, and try to optimise retry count for slow hardware.
82 """
83
84 machine.sleep(30)
85 machine.wait_for_text(text)
86
87 def toggle_maximise():
88 """
89 Maximise the current window.
90 """
91
92 machine.send_key("ctrl-meta_l-up")
93
94 # For some reason, Lomiri in these VM tests very frequently opens the starter menu a few seconds after sending the above.
95 # Because this isn't 100% reproducible all the time, and there is no command to await when OCR doesn't pick up some text,
96 # the best we can do is send some Escape input after waiting some arbitrary time and hope that it works out fine.
97 machine.sleep(5)
98 machine.send_key("esc")
99 machine.sleep(5)
100
101 def mouse_click(xpos, ypos):
102 """
103 Move the mouse to a screen location and hit left-click.
104 """
105
106 # Move
107 machine.execute(f"ydotool mousemove --absolute -- {xpos} {ypos}")
108 machine.sleep(2)
109
110 # Click (C0 - left button: down & up)
111 machine.execute("ydotool click 0xC0")
112 machine.sleep(2)
113
114 def open_starter():
115 """
116 Open the starter, and ensure it's opened.
117 """
118
119 # Using the keybind has a chance of instantly closing the menu again? Just click the button
120 mouse_click(15, 15)
121
122 '';
123
124 makeIndicatorTest =
125 {
126 name,
127 left,
128 ocr,
129 extraCheck ? null,
130
131 titleOcr,
132 }:
133
134 makeTest (
135 { pkgs, lib, ... }:
136 {
137 name = "lomiri-desktop-ayatana-indicator-${name}";
138
139 meta = {
140 maintainers = lib.teams.lomiri.members;
141 };
142
143 nodes.machine =
144 { config, ... }:
145 {
146 imports = [
147 ./common/auto.nix
148 ./common/user-account.nix
149 ];
150
151 virtualisation.memorySize = 2047;
152
153 users.users.${user} = {
154 inherit description password;
155 };
156
157 test-support.displayManager.auto = {
158 enable = true;
159 inherit user;
160 };
161
162 # To control mouse via scripting
163 programs.ydotool.enable = true;
164
165 services.desktopManager.lomiri.enable = lib.mkForce true;
166 services.displayManager.defaultSession = lib.mkForce "lomiri";
167
168 # Not setting wallpaper, as it breaks indicator OCR(?)
169 };
170
171 enableOCR = true;
172
173 testScript =
174 { nodes, ... }:
175 sharedTestFunctions
176 + ''
177 start_all()
178 machine.wait_for_unit("multi-user.target")
179
180 # The session should start, and not be stuck in i.e. a crash loop
181 with subtest("lomiri starts"):
182 machine.wait_until_succeeds("pgrep -u ${user} -f 'lomiri --mode=full-shell'")
183 # Output rendering from Lomiri has started when it starts printing performance diagnostics
184 machine.wait_for_console_text("Last frame took")
185 # Look for datetime's clock, one of the last elements to load
186 wait_for_text(r"(AM|PM)")
187 machine.screenshot("lomiri_launched")
188
189 # The ayatana indicators are an important part of the experience, and they hold the only graphical way of exiting the session.
190 # There's a test app we could use that also displays their contents, but it's abit inconsistent.
191 mouse_click(735, 0) # the cog in the top-right, for the session indicator
192 wait_for_text(${titleOcr})
193 machine.screenshot("indicators_open")
194
195 # Indicator order within the menus *should* be fixed based on per-indicator order setting
196 # Session is the one we clicked, but it might not be the one we want to test right now.
197 # Go as far left as necessary.
198 ${lib.strings.replicate left "machine.send_key(\"left\")\n"}
199
200 with subtest("ayatana indicator session works"):
201 wait_for_text(r"(${lib.strings.concatStringsSep "|" ocr})")
202 machine.screenshot("indicator_${name}")
203 ''
204 + lib.optionalString (extraCheck != null) extraCheck;
205 }
206 );
207
208 makeIndicatorTests =
209 {
210 titles,
211 details,
212 }:
213 let
214 titleOcr = "r\"(${builtins.concatStringsSep "|" titles})\"";
215 in
216 builtins.listToAttrs (
217 builtins.map (
218 {
219 name,
220 left,
221 ocr,
222 extraCheck ? null,
223 }:
224 {
225 name = "desktop-ayatana-indicator-${name}";
226 value = makeIndicatorTest {
227 inherit
228 name
229 left
230 ocr
231 extraCheck
232 titleOcr
233 ;
234 };
235 }
236 ) details
237 );
238in
239{
240 greeter = makeTest (
241 { pkgs, lib, ... }:
242 {
243 name = "lomiri-greeter";
244
245 meta = {
246 maintainers = lib.teams.lomiri.members;
247 };
248
249 nodes.machine =
250 { config, ... }:
251 {
252 imports = [ ./common/user-account.nix ];
253
254 virtualisation.memorySize = 2047;
255
256 users.users.${user} = {
257 inherit description password;
258 };
259
260 services.xserver.enable = true;
261 services.xserver.windowManager.icewm.enable = true;
262 services.xserver.displayManager.lightdm = {
263 enable = true;
264 greeters.lomiri.enable = true;
265 };
266 services.displayManager.defaultSession = lib.mkForce "none+icewm";
267 };
268
269 enableOCR = true;
270
271 testScript =
272 { nodes, ... }:
273 sharedTestFunctions
274 + ''
275 start_all()
276 machine.wait_for_unit("multi-user.target")
277
278 # Lomiri in greeter mode should work & be able to start a session
279 with subtest("lomiri greeter works"):
280 machine.wait_for_unit("display-manager.service")
281 machine.wait_until_succeeds("pgrep -u lightdm -f 'lomiri --mode=greeter'")
282
283 # Start page shows current time
284 wait_for_text(r"(AM|PM)")
285 machine.screenshot("lomiri_greeter_launched")
286
287 # Advance to login part
288 machine.send_key("ret")
289 wait_for_text("${description}")
290 machine.screenshot("lomiri_greeter_login")
291
292 # Login
293 machine.send_chars("${password}\n")
294 machine.wait_for_x()
295 machine.screenshot("session_launched")
296 '';
297 }
298 );
299
300 desktop-basics = makeTest (
301 { pkgs, lib, ... }:
302 {
303 name = "lomiri-desktop-basics";
304
305 meta = {
306 maintainers = lib.teams.lomiri.members;
307 };
308
309 nodes.machine =
310 { config, ... }:
311 {
312 imports = [
313 ./common/auto.nix
314 ./common/user-account.nix
315 ];
316
317 virtualisation.memorySize = 2047;
318
319 users.users.${user} = {
320 inherit description password;
321 };
322
323 test-support.displayManager.auto = {
324 enable = true;
325 inherit user;
326 };
327
328 # To control mouse via scripting
329 programs.ydotool.enable = true;
330
331 services.desktopManager.lomiri.enable = lib.mkForce true;
332 services.displayManager.defaultSession = lib.mkForce "lomiri";
333
334 # Help with OCR
335 fonts.packages = [ pkgs.inconsolata ];
336
337 environment = {
338 # Help with OCR
339 etc."xdg/alacritty/alacritty.toml".source = (pkgs.formats.toml { }).generate "alacritty.toml" {
340 font = rec {
341 normal.family = "Inconsolata";
342 bold.family = normal.family;
343 italic.family = normal.family;
344 bold_italic.family = normal.family;
345 size = 16;
346 };
347 colors = rec {
348 primary = {
349 foreground = "0x000000";
350 background = "0xffffff";
351 };
352 normal = {
353 green = primary.foreground;
354 };
355 };
356 };
357
358 etc."${wallpaperName}".source = wallpaperFile pkgs;
359
360 systemPackages = with pkgs; [
361 # Forcing alacritty to run as an X11 app when opened from the starter menu
362 (symlinkJoin {
363 name = "x11-${alacritty.name}";
364
365 paths = [ alacritty ];
366
367 nativeBuildInputs = [ makeWrapper ];
368
369 postBuild = ''
370 wrapProgram $out/bin/alacritty \
371 --set WINIT_UNIX_BACKEND x11 \
372 --set WAYLAND_DISPLAY ""
373 '';
374
375 inherit (alacritty) meta;
376 })
377 ];
378 };
379
380 programs.dconf.profiles.user.databases = [
381 (lomiriWallpaperDconfSettings pkgs)
382 ];
383
384 # Help with OCR
385 systemd.tmpfiles.settings = {
386 "10-lomiri-test-setup" = terminalOcrTmpfilesSetup { inherit pkgs lib config; };
387 };
388 };
389
390 enableOCR = true;
391
392 testScript =
393 { nodes, ... }:
394 sharedTestFunctions
395 + ''
396 start_all()
397 machine.wait_for_unit("multi-user.target")
398
399 # The session should start, and not be stuck in i.e. a crash loop
400 with subtest("lomiri starts"):
401 machine.wait_until_succeeds("pgrep -u ${user} -f 'lomiri --mode=full-shell'")
402 # Output rendering from Lomiri has started when it starts printing performance diagnostics
403 machine.wait_for_console_text("Last frame took")
404 # Look for datetime's clock, one of the last elements to load
405 wait_for_text(r"(AM|PM)")
406 machine.screenshot("lomiri_launched")
407
408 # Working terminal keybind is good
409 with subtest("terminal keybind works"):
410 machine.send_key("ctrl-alt-t")
411 wait_for_text(r"(${user}|machine)")
412 machine.screenshot("terminal_opens")
413 machine.send_key("alt-f4")
414
415 # We want the ability to launch applications
416 with subtest("starter menu works"):
417 open_starter()
418 machine.screenshot("starter_opens")
419
420 # Just try the terminal again, we know that it should work
421 machine.send_chars("Terminal\n")
422 wait_for_text(r"(${user}|machine)")
423 machine.send_key("alt-f4")
424
425 # We want support for X11 apps
426 with subtest("xwayland support works"):
427 open_starter()
428 machine.send_chars("Alacritty\n")
429 wait_for_text(r"(${user}|machine)")
430 machine.screenshot("alacritty_opens")
431 machine.send_key("alt-f4")
432
433 # Morph is how we go online
434 # Qt5 qtwebengine is not secure: https://github.com/NixOS/nixpkgs/pull/435067
435 # with subtest("morph browser works"):
436 # open_starter()
437 # machine.send_chars("Morph\n")
438 # wait_for_text(r"(Bookmarks|address|site|visited any)")
439 # machine.screenshot("morph_open")
440 #
441 # # morph-browser has a separate VM test to test its basic functionalities
442 #
443 # machine.send_key("alt-f4")
444
445 # LSS provides DE settings
446 with subtest("system settings open"):
447 open_starter()
448 machine.send_chars("System Settings\n")
449 wait_for_text("Rotation Lock")
450 machine.screenshot("settings_open")
451
452 # lomiri-system-settings has a separate VM test to test its basic functionalities
453
454 machine.send_key("alt-f4")
455 '';
456 }
457 );
458
459 desktop-appinteractions = makeTest (
460 { pkgs, lib, ... }:
461 {
462 name = "lomiri-desktop-appinteractions";
463
464 meta = {
465 maintainers = lib.teams.lomiri.members;
466 };
467
468 nodes.machine =
469 { config, ... }:
470 {
471 imports = [
472 ./common/auto.nix
473 ./common/user-account.nix
474 ];
475
476 virtualisation.memorySize = 2047;
477
478 users.users.${user} = {
479 inherit description password;
480 # polkit agent test
481 extraGroups = [ "wheel" ];
482 };
483
484 test-support.displayManager.auto = {
485 enable = true;
486 inherit user;
487 };
488
489 # To control mouse via scripting
490 programs.ydotool.enable = true;
491
492 services.desktopManager.lomiri.enable = lib.mkForce true;
493 services.displayManager.defaultSession = lib.mkForce "lomiri";
494
495 # Help with OCR
496 fonts.packages = [ pkgs.inconsolata ];
497
498 environment = {
499 # Help with OCR
500 etc."xdg/alacritty/alacritty.yml".text = lib.generators.toYAML { } {
501 font = rec {
502 normal.family = "Inconsolata";
503 bold.family = normal.family;
504 italic.family = normal.family;
505 bold_italic.family = normal.family;
506 size = 16;
507 };
508 colors = rec {
509 primary = {
510 foreground = "0x000000";
511 background = "0xffffff";
512 };
513 normal = {
514 green = primary.foreground;
515 };
516 };
517 };
518
519 etc."${wallpaperName}".source = wallpaperFile pkgs;
520
521 variables = {
522 # So we can test what lomiri-content-hub is working behind the scenes
523 LOMIRI_CONTENT_HUB_LOGGING_LEVEL = "2";
524 };
525
526 systemPackages = with pkgs; [
527 # For a convenient way of kicking off lomiri-content-hub peer collection
528 lomiri.lomiri-content-hub.examples
529 ];
530 };
531
532 programs.dconf.profiles.user.databases = [
533 (lomiriWallpaperDconfSettings pkgs)
534 ];
535
536 # Help with OCR
537 systemd.tmpfiles.settings = {
538 "10-lomiri-test-setup" = terminalOcrTmpfilesSetup { inherit pkgs lib config; };
539 };
540 };
541
542 enableOCR = true;
543
544 testScript =
545 { nodes, ... }:
546 sharedTestFunctions
547 + ''
548 start_all()
549 machine.wait_for_unit("multi-user.target")
550
551 # The session should start, and not be stuck in i.e. a crash loop
552 with subtest("lomiri starts"):
553 machine.wait_until_succeeds("pgrep -u ${user} -f 'lomiri --mode=full-shell'")
554 # Output rendering from Lomiri has started when it starts printing performance diagnostics
555 machine.wait_for_console_text("Last frame took")
556 # Look for datetime's clock, one of the last elements to load
557 wait_for_text(r"(AM|PM)")
558 machine.screenshot("lomiri_launched")
559
560 # Working terminal keybind is good
561 with subtest("terminal keybind works"):
562 machine.send_key("ctrl-alt-t")
563 wait_for_text(r"(${user}|machine)")
564 machine.screenshot("terminal_opens")
565
566 # for the LSS lomiri-content-hub test to work reliably, we need to kick off peer collecting
567 machine.send_chars("lomiri-content-hub-test-importer\n")
568 wait_for_text(r"(/build/source|hub.cpp|handler.cpp|void|virtual|const)") # awaiting log messages from lomiri-content-hub
569 machine.send_key("ctrl-c")
570
571 # Doing this here, since we need an in-session shell & separately starting a terminal again wastes time
572 with subtest("polkit agent works"):
573 machine.send_chars("pkexec touch /tmp/polkit-test\n")
574 # There's an authentication notification here that gains focus, but we struggle with OCRing it
575 # Just hope that it's up after a short wait
576 machine.sleep(10)
577 machine.screenshot("polkit_agent")
578 machine.send_chars("${password}")
579 machine.sleep(2) # Hopefully enough delay to make sure all the password characters have been registered? Maybe just placebo
580 machine.send_chars("\n")
581 machine.wait_for_file("/tmp/polkit-test", 10)
582
583 machine.send_key("alt-f4")
584
585 # LSS provides DE settings
586 with subtest("system settings open"):
587 open_starter()
588 machine.send_chars("System Settings\n")
589 wait_for_text("Rotation Lock")
590 machine.screenshot("settings_open")
591
592 # lomiri-system-settings has a separate VM test, only test Lomiri-specific lomiri-content-hub functionalities here
593
594 # Make fullscreen, can't navigate to Background plugin via keyboard unless window has non-phone-like aspect ratio
595 toggle_maximise()
596
597 # Load Background plugin
598 machine.send_key("tab")
599 machine.send_key("tab")
600 machine.send_key("tab")
601 machine.send_key("tab")
602 machine.send_key("tab")
603 machine.send_key("tab")
604 machine.send_key("ret")
605 wait_for_text("Background image")
606
607 # Try to load custom background
608 machine.send_key("shift-tab")
609 machine.send_key("shift-tab")
610 machine.send_key("shift-tab")
611 machine.send_key("shift-tab")
612 machine.send_key("shift-tab")
613 machine.send_key("shift-tab")
614 machine.send_key("ret")
615
616 # Peers should be loaded
617 wait_for_text("Gallery")
618 machine.screenshot("settings_lomiri-content-hub_peers")
619
620 # Select Gallery as content source
621 mouse_click(460, 80)
622
623 # Expect Gallery to be brought into the foreground, with its sharing page open
624 wait_for_text("Photos")
625
626 # If lomiri-content-hub encounters a problem, it may have crashed the original application issuing the request.
627 # Check that it's still alive
628 machine.succeed("pgrep -u ${user} -f lomiri-system-settings")
629
630 machine.screenshot("lomiri-content-hub_exchange")
631
632 # Testing any more would require more applications & setup, the fact that it's already being attempted is a good sign
633 machine.send_key("tab")
634 machine.send_key("ret")
635
636 machine.sleep(2) # sleep a tiny bit so gallery can close & the focus can return to LSS
637 machine.send_key("alt-f4")
638 '';
639 }
640 );
641
642 keymap =
643 let
644 pwInput = "qwerty";
645 pwOutput = "qwertz";
646 in
647 makeTest (
648 { pkgs, lib, ... }:
649 {
650 name = "lomiri-keymap";
651
652 meta = {
653 maintainers = lib.teams.lomiri.members;
654 };
655
656 nodes.machine =
657 { config, ... }:
658 {
659 imports = [ ./common/user-account.nix ];
660
661 virtualisation.memorySize = 2047;
662
663 users.users.${user} = {
664 inherit description;
665 password = lib.mkForce pwOutput;
666 };
667
668 services.desktopManager.lomiri.enable = lib.mkForce true;
669 services.displayManager.defaultSession = lib.mkForce "lomiri";
670
671 # Help with OCR
672 fonts.packages = [ pkgs.inconsolata ];
673
674 services.xserver.xkb.layout = lib.strings.concatStringsSep "," [
675 # Start with a non-QWERTY keymap to test keymap patch
676 "de"
677 # Then a QWERTY one to test switching
678 "us"
679 ];
680
681 environment.etc."${wallpaperName}".source = wallpaperFile pkgs;
682
683 programs.dconf.profiles.user.databases = [
684 (lomiriWallpaperDconfSettings pkgs)
685 ];
686
687 # Help with OCR
688 systemd.tmpfiles.settings = {
689 "10-lomiri-test-setup" = terminalOcrTmpfilesSetup { inherit pkgs lib config; };
690 };
691 };
692
693 enableOCR = true;
694
695 testScript =
696 { nodes, ... }:
697 sharedTestFunctions
698 + ''
699 start_all()
700 machine.wait_for_unit("multi-user.target")
701
702 # Lomiri in greeter mode should use the correct keymap
703 with subtest("lomiri greeter keymap works"):
704 machine.wait_for_unit("display-manager.service")
705 machine.wait_until_succeeds("pgrep -u lightdm -f 'lomiri --mode=greeter'")
706
707 # Start page shows current time
708 # And the greeter *actually* renders our wallpaper!
709 wait_for_text(r"(AM|PM|Lorem|ipsum)")
710 machine.screenshot("lomiri_greeter_launched")
711
712 # Advance to login part
713 machine.send_key("ret")
714 wait_for_text("${description}")
715 machine.screenshot("lomiri_greeter_login")
716
717 # Login
718 machine.send_chars("${pwInput}\n")
719 machine.wait_until_succeeds("pgrep -u ${user} -f 'lomiri --mode=full-shell'")
720
721 # Output rendering from Lomiri has started when it starts printing performance diagnostics
722 machine.wait_for_console_text("Last frame took")
723 # And the desktop doesn't render the wallpaper anymore. Grumble grumble...
724 # Look for datetime's clock, one of the last elements to load
725 wait_for_text(r"(AM|PM)")
726 machine.screenshot("lomiri_launched")
727
728 # Lomiri in desktop mode should use the correct keymap
729 with subtest("lomiri session keymap works"):
730 machine.send_key("ctrl-alt-t")
731 wait_for_text(r"(${user}|machine)")
732 machine.screenshot("terminal_opens")
733
734 machine.send_chars("touch ${pwInput}\n")
735 machine.wait_for_file("/home/alice/${pwOutput}", 90)
736
737 # Issues with this keybind: input leaks to focused surface, may open launcher
738 # Don't have the keyboard indicator to handle this better
739 machine.send_key("meta_l-spc")
740 machine.wait_for_console_text('SET KEYMAP "us"')
741
742 # Handle keybind fallout
743 machine.sleep(10) # wait for everything to settle
744 machine.send_key("esc") # close launcher in case it was opened
745 machine.sleep(2) # wait for animation to finish
746 # Make sure input leaks are gone
747 machine.send_key("backspace")
748 machine.send_key("backspace")
749 machine.send_key("backspace")
750 machine.send_key("backspace")
751 machine.send_key("backspace")
752 machine.send_key("backspace")
753 machine.send_key("backspace")
754 machine.send_key("backspace")
755 machine.send_key("backspace")
756 machine.send_key("backspace")
757
758 machine.send_chars("touch ${pwInput}\n")
759 machine.wait_for_file("/home/alice/${pwInput}", 90)
760
761 machine.send_key("alt-f4")
762 '';
763 }
764 );
765}
766// makeIndicatorTests {
767 titles = [
768 "Notifications" # messages
769 "Rotation" # display
770 "Battery" # power
771 "Sound" # sound
772 "Time" # datetime
773 "Date" # datetime
774 "System" # session
775 ];
776 details = [
777 # messages normally has no contents
778 ({
779 name = "display";
780 left = 6;
781 ocr = [ "Lock" ];
782 })
783 ({
784 name = "bluetooth";
785 left = 5;
786 ocr = [ "Bluetooth" ];
787 })
788 ({
789 name = "network";
790 left = 4;
791 ocr = [
792 "Flight"
793 "Wi-Fi"
794 ];
795 })
796 ({
797 name = "sound";
798 left = 3;
799 ocr = [
800 "Silent"
801 "Volume"
802 ];
803 })
804 ({
805 name = "power";
806 left = 2;
807 ocr = [
808 "Charge"
809 "Battery"
810 ];
811 })
812 ({
813 name = "datetime";
814 left = 1;
815 ocr = [
816 "Time"
817 "Date"
818 ];
819 })
820 ({
821 name = "session";
822 left = 0;
823 ocr = [ "Log Out" ];
824 extraCheck = ''
825 # We should be able to log out and return to the greeter
826 mouse_click(600, 250) # "Log Out"
827 mouse_click(340, 220) # confirm logout
828 machine.wait_until_fails("pgrep -u ${user} -f 'lomiri --mode=full-shell'")
829 '';
830 })
831 ];
832}