diff options
| author | Martin Weinelt <hexa@darmstadt.ccc.de> | 2021-05-06 14:12:06 +0200 |
|---|---|---|
| committer | Martin Weinelt <hexa@darmstadt.ccc.de> | 2023-02-19 14:11:56 +0100 |
| commit | 9aeeb5b8efb9ceaad69a7d216ea1883337ec2a40 (patch) | |
| tree | 5c8f524265129d992bd90a3e13f9aacf5fca12d7 | |
| parent | firejail: Fix double-dash usage on non-POSIX shells (diff) | |
| download | nixpkgs-origin/hass-onboarding.tar.gz | |
WIP: nixos/tests/home-assistant: test onboarding floworigin/hass-onboarding
| -rw-r--r-- | nixos/tests/all-tests.nix | 2 | ||||
| -rw-r--r-- | nixos/tests/home-assistant/default.nix (renamed from nixos/tests/home-assistant.nix) | 36 | ||||
| -rw-r--r-- | nixos/tests/home-assistant/webdriver.py | 252 |
3 files changed, 286 insertions, 4 deletions
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 86dd096afc0b..e50267310bb6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -291,7 +291,7 @@ in { hledger-web = handleTest ./hledger-web.nix {}; hocker-fetchdocker = handleTest ./hocker-fetchdocker {}; hockeypuck = handleTest ./hockeypuck.nix { }; - home-assistant = handleTest ./home-assistant.nix {}; + home-assistant = handleTest ./home-assistant {}; hostname = handleTest ./hostname.nix {}; hound = handleTest ./hound.nix {}; hub = handleTest ./git/hub.nix {}; diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant/default.nix index 8d58de75eabc..c16a6c0b3224 100644 --- a/nixos/tests/home-assistant.nix +++ b/nixos/tests/home-assistant/default.nix @@ -1,12 +1,16 @@ -import ./make-test-python.nix ({ pkgs, lib, ... }: +import ../make-test-python.nix ({ pkgs, lib, ... }: let configDir = "/var/lib/foobar"; + userName = "admin"; + password = "secret"; in { name = "home-assistant"; meta.maintainers = lib.teams.home-assistant.members; nodes.hass = { pkgs, ... }: { + virtualisation.memorySize = 1024; + services.postgresql = { enable = true; ensureDatabases = [ "hass" ]; @@ -27,7 +31,10 @@ in { extraPackages = ps: with ps; [ colorama ]; - extraComponents = [ "zha" ]; + extraComponents = [ + "met" + "radio_browser" + ]; }).overrideAttrs (oldAttrs: { doInstallCheck = false; }); @@ -79,6 +86,7 @@ in { # https://www.home-assistant.io/integrations/logger/ logger = { default = "info"; + logs."homeassistant.components.http" = "debug"; }; }; @@ -108,6 +116,22 @@ in { inheritParentConfig = true; configuration.services.home-assistant.config.esphome = {}; }; + + environment.systemPackages = let + testRunner = pkgs.writers.writePython3Bin "test-runner" { + libraries = with pkgs.python3Packages; [ + selenium + structlog + ]; + flakeIgnore = [ + "E501" # line too long + ]; + } (builtins.readFile ./webdriver.py); + in with pkgs; [ + chromium + chromedriver + testRunner + ]; }; testScript = { nodes, ... }: let @@ -150,7 +174,7 @@ in { with subtest("Check extraComponents and extraPackages are considered from the package"): hass.succeed(f"grep -q 'colorama' {package}/extra_packages") - hass.succeed(f"grep -q 'zha' {package}/extra_components") + hass.succeed(f"grep -q 'radio_browser' {package}/extra_components") with subtest("Check extraComponents and extraPackages are considered from the module"): hass.succeed(f"grep -q 'psycopg2' {package}/extra_packages") @@ -192,5 +216,11 @@ in { with subtest("Check systemd unit hardening"): hass.log(hass.succeed("systemctl cat home-assistant.service")) hass.log(hass.succeed("systemd-analyze security home-assistant.service")) + + with subtest("Test onboarding"): + hass.execute( + "systemd-run --wait --unit hass-onboarding -E PATH=${pkgs.geckodriver}/bin:$PATH -E PYTHONUNBUFFERED=1 test-runner" + ) + hass.copy_from_vm("/run/artifacts") ''; }) diff --git a/nixos/tests/home-assistant/webdriver.py b/nixos/tests/home-assistant/webdriver.py new file mode 100644 index 000000000000..bfa6722c29d8 --- /dev/null +++ b/nixos/tests/home-assistant/webdriver.py @@ -0,0 +1,252 @@ +from dataclasses import dataclass +from os import mkdir +import os.path +import time +from typing import Set + +import structlog +from structlog.contextvars import bind_contextvars, reset_contextvars + +# shadow root support only in chromium based browsers as of selenium 4.8.0 +# https://github.com/SeleniumHQ/selenium/blob/selenium-4.8.0/py/selenium/webdriver/remote/webelement.py#L245-L248 +from selenium.webdriver import Chrome +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.alert import Alert +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +log = structlog.get_logger() + + +# encapsulate selectors to find html elements +@dataclass +class S: + by: By + value: str + + # retrieve the shadow dom of the tag + shadow_root: bool = False + + # retrieve all matching tags + multiple: bool = False + + +# helper function to navigate through nested shadow roots +def select(*selectors: S, origin): + doc = origin + for sel in selectors: + if sel.multiple: + if isinstance(doc, list): + raise RuntimeError( + "The multiple qualifier can only appear once in a selection group" + ) + else: + doc = doc.find_elements(sel.by, sel.value) + if sel.shadow_root: + doc = [x.shadow_root for x in doc] + else: + if isinstance(doc, list): + doc = [x.find_element(sel.by, sel.value) for x in doc] + if sel.shadow_root: + doc = [x.shadow_root for x in doc] + else: + doc = doc.find_element(sel.by, sel.value) + if sel.shadow_root: + doc = doc.shadow_root + return doc + + +# Custom wait implementation for nested elements +class NestedElementPresent: + def __init__(self, *selectors: S): + self.selectors = selectors + + def __call__(self, driver): + result = select(*self.selectors, origin=driver) + return result + + +def write(input, value: str, field_name: str): + input.click() + # input.clear() does not work properly :( + for _ in range(16): + input.send_keys(Keys.BACKSPACE) + input.send_keys(value) + log.info(f"Set {field_name}", value=value) + + +ARTIFACT_PATH = "/run/artifacts" +mkdir(ARTIFACT_PATH) + +options = Options() +options.add_argument("--headless") +options.add_argument("--no-sandbox") # run as root +options.binary_location = "/run/current-system/sw/bin/chromium" +driver = Chrome( + options=options, +) +wait = WebDriverWait(driver, 10) + +# increase viewport, so everything becomes visible w/o scrolling +driver.set_window_size(720, 1280) + + +def onboarding(): + # wait for the site to load + url = "http://localhost:8123/onboarding.html" + log.info("Loading", url=url) + driver.get(url) + wait.until(EC.title_contains("Home Assistant")) + log.info("Ready", url=url) + + # move the viewport around to trigger a redraw + body = driver.find_element(By.TAG_NAME, "body") + body.send_keys(Keys.SPACE) + body.send_keys(Keys.SHIFT + Keys.SPACE) + + onboarding_step_user() + onboarding_step_core_config() + onboarding_step_analytics() + + +@dataclass +class Credentials: + username: str + password: str + + +def onboarding_step_user() -> Credentials: + ctx = bind_contextvars(step="user") + + # wait until the page is rendered + log.info("Waiting for onboarding-create-user") + onboarding_create_user = wait.until( + NestedElementPresent( + S(By.CSS_SELECTOR, "ha-onboarding", shadow_root=True), + S(By.CSS_SELECTOR, "onboarding-create-user", shadow_root=True), + ) + ) + log.info("Found onboarding-create-user", tag=onboarding_create_user) + + driver.save_screenshot(os.path.join(ARTIFACT_PATH, "onboarding_step1.png")) + + name, username, password, confirm = select( + S(By.CSS_SELECTOR, "ha-form", shadow_root=True), + S(By.CSS_SELECTOR, "ha-selector", shadow_root=True, multiple=True), + S(By.CSS_SELECTOR, "ha-selector-text", shadow_root=True), + S(By.CSS_SELECTOR, "ha-textfield", shadow_root=True), + S(By.CSS_SELECTOR, "input.mdc-text-field__input"), + origin=onboarding_create_user, + ) + + creds = Credentials("nixos", "test") + + write(name, creds.username, "Name") + write(username, "NixOS Testuser", "Username") + write(password, creds.password, "Password") + write(confirm, creds.password, "Confirm password") + + driver.save_screenshot( + os.path.join(ARTIFACT_PATH, "onboarding_step1_logindata.png") + ) + + log.info("Submit") + select( + S(By.CSS_SELECTOR, "mwc-button"), origin=onboarding_create_user + ).click() + + reset_contextvars(**ctx) + + return creds + + +def onboarding_step_core_config(): + ctx = bind_contextvars(step="core_config") + + # wait until the page is rendered + log.info("Waiting for onboarding-create-user") + onboarding_core_config = wait.until( + NestedElementPresent( + S(By.CSS_SELECTOR, "ha-onboarding", shadow_root=True), + S(By.CSS_SELECTOR, "onboarding-core-config", shadow_root=True), + ) + ) + log.info("Found onboarding-core-config") + + driver.save_screenshot(os.path.join(ARTIFACT_PATH, "onboarding_step2.png")) + + # set home name + home = select( + S(By.CSS_SELECTOR, "ha-textfield", shadow_root=True), + S(By.CSS_SELECTOR, "input"), + origin=onboarding_core_config, + ) + write(home, "NixOS Testdriver", "Installation name") + + # try environment detection, will fail and only set language to "en" + select( + S(By.CSS_SELECTOR, "div.middle-text mwc-button"), origin=onboarding_core_config + ).click() + log.info("Detect environment") + + driver.save_screenshot(os.path.join(ARTIFACT_PATH, "onboarding_step2_detect.png")) + + map_marker = select( + S(By.CSS_SELECTOR, "ha-locations-editor", shadow_root=True), + S(By.CSS_SELECTOR, "ha-map", shadow_root=True), + S(By.CSS_SELECTOR, "div.leaflet-marker-pane img"), + origin=onboarding_core_config + ) + log.info("Found leaflet-marker-pane", tag=map_marker) + ActionChains(driver).click().click_and_hold().move_by_offset(100, 100).release().perform() + log.info("Moved location marker") + + country, lang, tz, elevation, currency = select( + S(By.CSS_SELECTOR, "div.row ha-textfield", shadow_root=True, multiple=True), + S(By.CSS_SELECTOR, "input"), + origin=onboarding_core_config, + ) + write(country, "DE", "Country") + print(tz.tag_name) + write(tz, "UTC", "Time Zone") + write(elevation, "123", "Elevation") + write(currency, "EUR", "Currency") + + driver.save_screenshot(os.path.join(ARTIFACT_PATH, "onboarding_step2_inputs.png")) + + log.info("Submit") + select( + S(By.CSS_SELECTOR, "div.footer mwc-button"), origin=onboarding_core_config + ).click() + + Alert(driver).accept() + + driver.save_screenshot(os.path.join(ARTIFACT_PATH, "onboarding_step2_submit.png")) + + reset_contextvars(**ctx) + + + +def onboarding_step_analytics(): + ctx = bind_contextvars(step="analytics") + + # wait until the page is rendered + log.info("Waiting for onboarding-analytics") + onboarding_analytics = wait.until( + NestedElementPresent( + S(By.CSS_SELECTOR, "ha-onboarding", shadow_root=True), + S(By.CSS_SELECTOR, "onboarding-analytics", shadow_root=True), + ) + ) + log.info("Found onboarding-analytics", tag=onboarding_analytics) + + driver.save_screenshot(os.path.join(ARTIFACT_PATH, "onboarding_step3.png")) + + +onboarding() + +driver.close() +print("close") |
