nixos/goupile: add basic playwright smoke test

Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
Co-authored-by: Sizhe Zhao <prc.zhao@outlook.com>
This commit is contained in:
phanirithvij
2026-03-18 20:11:09 +05:30
co-authored by Sizhe Zhao
parent c70e3c4402
commit 7203310af6
2 changed files with 272 additions and 9 deletions
@@ -0,0 +1,178 @@
import os
import openpyxl
import tempfile
from playwright.sync_api import sync_playwright
BASE_URL = "http://localhost:8889"
# NOTE: these are the passwords
ADMIN_PASSWD = "car-shop-in-the-mall"
ALICE_PASSWD = "user-goes-to-the-car-shop"
def run_test():
is_headful = os.getenv("HEADFUL") == "1"
with sync_playwright() as p:
browser = p.chromium.launch(headless=not is_headful)
context = browser.new_context(
accept_downloads=True, record_video_dir="/tmp/videos/"
)
# more default timeout for slow nixos test vms
context.set_default_timeout(90 * 1000)
page = context.new_page()
page.goto(f"{BASE_URL}/admin")
# admin and doman setup
page.get_by_role("textbox", name="Domain name *").fill("domain")
page.get_by_role("textbox", name="Domain title *").fill("domain")
page.get_by_role("textbox", name="Password *").fill(ADMIN_PASSWD)
page.get_by_role("textbox", name="Confirmation").fill(ADMIN_PASSWD)
page.get_by_role("textbox", name="Decryption key *").click()
page.get_by_role("button", name="Installer").click()
# login to admin dashboard as admin
page.get_by_role("textbox", name="Username *").fill("admin")
page.get_by_role("textbox", name="Password *").fill(ADMIN_PASSWD)
page.get_by_role("button", name="Login").click()
# create a sample project, it will switch the view to project's configure page
page.get_by_text("Create new project").click()
page.get_by_role("textbox", name="Name *").fill("proj1")
page.get_by_role("button", name="Create").click()
# create a test non-root user, alice
page.get_by_text("Create new user").click()
page.get_by_role("textbox", name="Username *").fill("alice")
page.get_by_role("button", name="No", exact=True).click()
page.get_by_role("textbox", name="Password *").fill(ALICE_PASSWD)
page.get_by_role("textbox", name="Confirmation").fill(ALICE_PASSWD)
page.get_by_role("button", name="Create").click()
# give alice, permissions to access the project
page.get_by_role("button", name="Assign").nth(1).click()
page.get_by_text("Read", exact=True).click()
page.get_by_text("Save", exact=True).click()
page.get_by_text("Export", exact=True).click()
page.get_by_text("Download", exact=True).click()
# Open the project in new page
page.locator("form").get_by_role("button", name="Edit").click()
with page.expect_popup() as page1_info:
page.get_by_role("link", name="access").click()
page1 = page1_info.value
page1.set_default_timeout(120 * 1000)
# fill entries as admin (enter 1 for everything)
page1.get_by_role("button", name="Create new record").click()
page1.locator("#ins_tiles").get_by_text("Introduction").click()
page1.get_by_role("textbox", name="Inclusion date *").fill("2000-01-01")
page1.get_by_role("spinbutton", name="Age *").click()
page1.get_by_role("spinbutton", name="Age *").fill("1")
page1.get_by_role("button", name="Save").click()
page1.wait_for_timeout(1000)
page1.get_by_role("button", name="Advanced").click()
page1.get_by_role("spinbutton", name="Age *").click()
page1.get_by_role("spinbutton", name="Age *").fill("1")
page1.get_by_role("button", name="Save").click()
page1.wait_for_timeout(1000)
page1.get_by_role("button", name="Page layout").click()
page1.get_by_role("spinbutton", name="Variable A1").fill("1")
page1.get_by_role("button", name="Save").click()
page1.wait_for_timeout(1000)
# create export #1
page1.get_by_role("button", name="Data").click()
page1.wait_for_timeout(1000)
page1.get_by_role("button", name="Data exports").click()
with page1.expect_download() as download_info:
page1.get_by_role("button", name="Create export").click()
# logout as admin
page.get_by_role("button", name="admin", exact=True).click()
with page.expect_popup() as page2_info:
page.get_by_role("link", name="access").click()
page2 = page2_info.value
page2.set_default_timeout(120 * 1000)
page2.get_by_role("button", name="admin").click()
page2.get_by_role("button", name="Logout").click()
# login as alice
page2.get_by_role("textbox", name="Username *").fill("alice")
page2.get_by_role("textbox", name="Password *").fill(ALICE_PASSWD)
page2.get_by_role("button", name="Login").click()
# create entry as alice (fill `2` for everything)
page2.get_by_role("button", name="Create new record").click()
page2.get_by_text("1 Introduction").click()
page2.get_by_role("textbox", name="Inclusion date *").fill("2000-01-01")
page2.get_by_role("spinbutton", name="Age *").click()
page2.get_by_role("spinbutton", name="Age *").fill("2")
page2.get_by_role("button", name="Save").click()
page2.wait_for_timeout(1000)
page2.get_by_role("button", name="Advanced").click()
page2.get_by_role("spinbutton", name="Age *").click()
page2.get_by_role("spinbutton", name="Age *").fill("2")
page2.get_by_role("button", name="Save").click()
page2.wait_for_timeout(1000)
page2.get_by_role("button", name="Page layout").click()
page2.get_by_role("spinbutton", name="Variable A1").click()
page2.get_by_role("spinbutton", name="Variable A1").fill("2")
page2.get_by_role("button", name="Save").click()
page2.wait_for_timeout(1000)
# create export #2
page2.get_by_role("button", name="Data").click()
page2.wait_for_timeout(1000)
page2.get_by_role("button", name="Data exports").click()
page2.get_by_role("button", name="Previous exports").click()
with page2.expect_download() as download1_info:
page2.locator("a").filter(has_text="Download").click()
download1 = download1_info.value
save_path1 = os.path.join(tempfile.gettempdir(), download1.suggested_filename)
download1.save_as(save_path1)
print(f"exported all records to {save_path1}")
page2.get_by_role("button", name="Data exports").click()
with page2.expect_download() as download2_info:
page2.get_by_role("button", name="Create export").click()
download2 = download2_info.value
save_path2 = os.path.join(tempfile.gettempdir(), download2.suggested_filename)
download2.save_as(save_path2)
print(f"exported all records to {save_path2}")
context.close()
browser.close()
# check that exported files have correct entries
wb1 = openpyxl.load_workbook(save_path1)
for sheet, cell in zip(["intro", "advanced", "layout"], ["D2", "D2", "C2"]):
val = wb1[sheet][cell].value
assert val == 1, f"Sheet {sheet}, Cell {cell}: Expected 1 (admin), got {val}"
wb2 = openpyxl.load_workbook(save_path2)
for sheet, cell in zip(["intro", "advanced", "layout"], ["D3", "D3", "C3"]):
val = wb2[sheet][cell].value
assert val == 2, f"Sheet {sheet}, Cell {cell}: Expected 2 (alice), got {val}"
print("Test passed successfully!")
if __name__ == "__main__":
run_test()
+94 -9
View File
@@ -1,7 +1,62 @@
{ lib, pkgs, ... }:
{
lib,
pkgs,
...
}:
let
python = pkgs.python3.withPackages (
ps: with ps; [
requests
playwright
openpyxl
]
);
runScript = "${lib.getExe python} ${./basic_interaction_test.py}";
run-goupile-test = pkgs.writeShellScriptBin "run-goupile-test" ''
set -euo pipefail
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# check if attached to a terminal
if [ -t 1 ]; then
# interactive testing
export HEADFUL=''${HEADFUL:-1}
export PWDEBUG=''${PWDEBUG:-0}
export DISPLAY=''${DISPLAY:-:0}
if [ "$(id -u)" = "0" ] && [ -d "/home/alice" ]; then
runuser -u alice \
-w DISPLAY,HEADFUL,PWDEBUG,PLAYWRIGHT_BROWSERS_PATH,PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD \
-- ${runScript}
else
${runScript}
fi
else
# non-interactive nixos test
# Print instructions to the nix logs
cat <<'EOF' | tee >(systemd-cat -t goupile-e2e)
================================================================================
NOTE: The goupile e2e test can be run interactively either inside the vm or on the host
- First, run `nix-build -A nixosTests.goupile.driverInteractive` and `./result/bin/nixos-test-driver`
- Run `start_all()` inside the repl
- Then `$(nix-build -A nixosTests.goupile.interactive-script)/bin/run-goupile-test` to run the full test interactively
- Or `env PWDEBUG=1 $(nix-build -A nixosTests.goupile.interactive-script)/bin/run-goupile-test` to show the playwright inspector to debug
================================================================================
EOF
echo "Starting smoke test..." | systemd-cat -t goupile-e2e
${runScript} 2>&1 | tee >(systemd-cat -t goupile-e2e)
fi
'';
in
{
name = "goupile";
passthru.interactive-script = run-goupile-test;
nodes.machine =
{
lib,
@@ -21,6 +76,22 @@
hostName = "goupile";
domain = "local";
};
# goupile tries to resolve it at runtime, resolve it instead of patching it out
# as the dns resolution step serves a purpose, to force glibc to load NSS libraries
# see server/goupile.cc and search for getaddrinfo or www.example.com
networking.extraHosts = ''
127.0.0.1 www.example.com
'';
environment.systemPackages = [
python
run-goupile-test
];
# more cores and memory to improve chromium performance
virtualisation.memorySize = lib.mkForce 8192;
virtualisation.cores = 4;
};
testScript =
@@ -30,6 +101,7 @@
in
# py
''
import os
start_all()
machine.wait_for_unit("goupile.service")
@@ -38,10 +110,15 @@
machine.succeed("curl -q http://localhost:${port}")
machine.succeed("curl -q http://goupile.local")
machine.succeed("curl -q http://localhost")
machine.succeed("run-goupile-test")
out_dir = os.environ.get("out", os.getcwd())
machine.copy_from_vm("/tmp/videos", out_dir)
'';
# Debug interactively with:
# - nix run .#nixosTests.goupile.driverInteractive -L
# - nix-build -A nixosTests.goupile.driverInteractive
# - ./result/bin/nixos-test-driver
# - run_tests()
# ssh -o User=root vsock%3 (can also do vsock/3, but % works with scp etc.)
interactive.sshBackdoor.enable = true;
@@ -52,16 +129,24 @@
port = config.services.goupile.settings.HTTP.Port;
in
{
virtualisation.forwardPorts = map (port: {
from = "host";
host.port = port;
guest.port = port;
}) [ port ];
imports = [
# enable graphical session + users (alice, bob)
../../common/x11.nix
../../common/user-account.nix
];
services.xserver.enable = true;
test-support.displayManager.auto.user = "alice";
virtualisation.forwardPorts = [
{
from = "host";
host.port = port;
guest.port = port;
}
];
# forwarded ports need to be accessible
networking.firewall.allowedTCPPorts = [ port ];
virtualisation.graphics = false;
};
meta.maintainers = lib.teams.ngi.members;