Verified Commit abd6b85f authored by Karel Koci's avatar Karel Koci 🤘
Browse files

tests/reforis: add guide tests

These are simple "click trough" tests of reForis guide.

It only ensures that we can correctly pass through guide. The tests of
dialogues we are passing trough should be implemented.
parent c4e16a31
......@@ -11,7 +11,7 @@ NSFarm utilizes tools present on standard Linux based PC. It is based on Python3
pytest, pexpect and LXD.
You need following software and its dependencies:
* Python3 (>=3.6)
* Python3 (>=3.9)
* pytest (>=5.0)
* pytest-html (>=2.0)
* pexpect
......
from .container import Container
from . import reforis
from .container import Container, DRIVER_PORTS as _DRIVER_PORTS
__all__ = ["reforis", "Container"]
BROWSERS = _DRIVER_PORTS.keys()
"""Base classes for all other implementations used in web testing.
"""
import abc
import logging
import time
import selenium
from selenium.webdriver.common.by import By as _By
from selenium.webdriver.support import expected_conditions as _ec
from selenium.webdriver.support.ui import WebDriverWait as _Wait
logger = logging.getLogger(__package__)
class Element:
"""Wrapper class around Selenium's WebElement. It provides additional functionality."""
def __init__(self, webdriver: selenium.webdriver.remote.webdriver.WebDriver, xpath: str, timeout=10):
self.webdriver = webdriver
self.xpath = xpath
self._element = _Wait(self.webdriver, timeout).until(_ec.presence_of_element_located((_By.XPATH, xpath)))
def __getattr__(self, attr):
return getattr(self._element, attr)
def wait(self, expected_condition, *args, timeout=10, **kwargs):
"""Wait for given condition. This is helper just for waiting"""
_Wait(self.webdriver, timeout).until(expected_condition((_By.XPATH, self.xpath), *args, **kwargs))
def click(self, timeout=10, retry=5):
"""Click the element.
This first waits for element being clickable and then it tries to click it.
The combination of Selenium and React is little bit wonky and sometimes we have to try multiple times and thus
this also allows set retries. This is required for example for checkboxes.
"""
self.wait(_ec.element_to_be_clickable, timeout=timeout)
for _ in range(retry):
try:
self._element.click()
return
except selenium.common.exceptions.ElementClickInterceptedException as exc:
logger.debug("Click attempt on '%s' failed: %s", self._element, exc)
time.sleep(1)
@property
def cls(self):
"""The easy way to access list of classes assigned to element."""
return self._element.get_attribute("class").split()
class Page(abc.ABC):
"""Abstract page representation.
The child is required to change or expand following class variables:
_HREF: Hyperlink to this page relative to reForis URL
_ID: Identifier used to check if user is looking at the correct page.
_ELEMENTS: All elements exported by page for easy use
"""
_HREF: str = ""
_ID: str = "//"
_ELEMENTS: dict[str, str] = dict()
def __init__(self, webdriver: selenium.webdriver.remote.webdriver.WebDriver, url: str = "http://192.168.1.1/"):
self.url = url
self.webdriver = webdriver
def __getattr__(self, name):
if name not in self._ELEMENTS:
raise AttributeError
return self.element(self._ELEMENTS[name])
def _queryfunc(self, xpath):
@property
def query(self):
return self.element(xpath)
return query
def go(self):
"""Navigate to this page."""
self.webdriver.get(self.url + self._HREF)
def verify(self, timeout=10):
"""Check that we are really looking at this page by locating some specific element.
Returns boolean.
"""
try:
self.element(self._ID, timeout)
return True
except selenium.common.exceptions.NoSuchElementException:
return False
def element(self, xpath, timeout=10):
"""Locates given element and returns it."""
return Element(self.webdriver, xpath, timeout=timeout)
"""Extended LXD container implementation just for Selenium container.
"""
import typing
import subprocess
import contextlib
import subprocess
import typing
import selenium.webdriver
from ..lxd import LXDConnection, Container as LXDContainer
from ..lxd import Container as LXDContainer
from ..lxd import LXDConnection
IMAGE = "selenium"
......@@ -18,12 +21,17 @@ RESOLUTION = [1366, 769]
class Container(LXDContainer):
"""Container with WebDrivers to run Selenium tests against.
"""
"""Container with WebDrivers to run Selenium tests against."""
open_viewer = False
def __init__(self, lxd_connection: LXDConnection, device_map: dict = None, internet: typing.Optional[bool] = None,
strict: bool = True):
def __init__(
self,
lxd_connection: LXDConnection,
device_map: dict = None,
internet: typing.Optional[bool] = None,
strict: bool = True,
):
super().__init__(lxd_connection, IMAGE, device_map, internet, strict)
self._viewer = None
self._viewer_port = None
......@@ -35,7 +43,7 @@ class Container(LXDContainer):
self._viewer_port = self.network.proxy_open(port=5900)
self.shell.run("wait4tcp 5900")
self._logger.info("Running: vncviewer localhost:%d", self._viewer_port)
self._viewer = subprocess.Popen(["vncviewer", f'localhost:{self._viewer_port}'])
self._viewer = subprocess.Popen(["vncviewer", f"localhost:{self._viewer_port}"])
def cleanup(self):
if self._viewer is not None:
......
"""Implementation of varous helper expected conditions.
"""
class element_has_class(object):
"""An expectation for checking that an element has a particular css class.
locator - used to find the element
invert - invert the check
returns the WebElement once it has the particular css class
"""
def __init__(self, locator, cls, invert=False):
self.locator = locator
self.cls = cls
self.invert = invert
def __call__(self, driver):
el = driver.find_element(*self.locator)
classes = el.get_attribute("class").split()
if (self.cls in classes) != self.invert:
return el
return False
from .top import *
from . import admin
from . import guide
from . import network
from . import packages
__all__ = [
"ReForis", "Overview",
"admin", "guide", "network", "packages"
]
"""reForis administration page objects.
"""
from .top import ReForis
class Password(ReForis):
"""Dialog to set password."""
_HREF = ReForis._HREF + "/administration/password"
_ID = "//h1[text()='Password']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"current": "//form[1]//label[text()='Current password']/..//input",
"new1": "//form[1]//label[text()='New password']/..//input",
"new2": "//form[1]//label[text()='Confirm new password']/..//input",
"use4root": "//form[1]//input[@type='checkbox']",
"save": "//form[1]//button[@type='submit']",
}
class RegionAndTime(ReForis):
"""Dialog to set region and time."""
_HREF = ReForis._HREF + "/administration/region-and-time"
_ID = "//h1[text()='Region and Time']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"save": "(//form)[1]//button[@type='submit']",
}
"""reForis guide page objects.
"""
from .. import ec
from .top import ReForis
class Guide(ReForis):
"""Guide control and other wrapper features added on top of basic dialogues."""
_HREF = ReForis._HREF + "/guide"
_ID = "//div[@id='guide-container']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"next": "//span[text()='Next step']/..",
"skip": "//span[text()='Skip guide']/..",
}
def wait4ready(self, timeout=10):
"""Wait for ready to go to next step."""
self.next.wait(ec.element_has_class, "disabled", True, timeout=timeout)
class Workflow(ReForis):
"""Guide workflow selection."""
_HREF = ReForis._HREF + "/guide/profile"
_ID = "//h1[text()='Guide Workflow']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"router": "//div[@id='workflow-selector']/div/div[1]//button",
"minimal": "//div[@id='workflow-selector']/div/div[2]//button",
"server": "//div[@id='workflow-selector']/div/div[3]//button",
}
class Finished(ReForis):
"""Guide finished page."""
_HREF = ReForis._HREF + "/guide/finished"
_ID = "//h2[text()='Guide Finished']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"cont": "//button[text()='Continue']",
}
"""reForis network page objects.
"""
from .top import ReForis
class Wan(ReForis):
"""WAN interface configuration."""
_HREF = ReForis._HREF + "/network-settings/wan"
_ID = "//h1[text()='WAN']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"save": "(//form)[1]//button[@type='submit']",
}
class Lan(ReForis):
"""LAN interface configuration."""
_HREF = ReForis._HREF + "/network-settings/lan"
_ID = "//h1[text()='LAN']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"save": "(//form)[1]//button[@type='submit']",
}
class DNS(ReForis):
"""LAN interface configuration."""
_HREF = ReForis._HREF + "/network-settings/dns"
_ID = "//h1[text()='DNS']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"save": "(//form)[1]//button[@type='submit']",
}
class Interfaces(ReForis):
"""Network interfaces assigment."""
_HREF = ReForis._HREF + "/network-settings/interfaces"
_ID = "//h1[text()='Network Interfaces']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"save": "(//form)[1]//button[@type='submit']",
}
"""reForis packages page objects.
"""
from .top import ReForis
class UpdateSettings(ReForis):
"""Update settings page."""
_HREF = ReForis._HREF + "/package-management/update-settings"
_ID = "//h1[text()='Update Settings']"
_ELEMENTS = {
**ReForis._ELEMENTS,
"save": "(//form)[1]//button[@type='submit']",
}
"""reForis top level page representation objects.
This contains base for all pages as well as overview or about page.
"""
from .. import base
class ReForis(base.Page):
"""Generic reForis page."""
_HREF = "reforis"
def notification_close(self):
"""Wait for notification to appear and close it."""
self.element("//div[@id='alert-container']//button").click()
class Overview(ReForis):
"""Overview page. This page is the index of reForis."""
_HREF = ReForis._HREF + "/overview"
_ID = "//h1[text()='Overview']"
"""Simple test to verify that our Selenium setup works as expected.
"""
import pytest
from nsfarm.web import Container
from nsfarm.web.container import DRIVER_PORTS
from nsfarm.web import Container, BROWSERS
# pylint: disable=no-self-use
......@@ -12,7 +13,7 @@ def fixture_webcontainer(lxd_connection):
yield container
@pytest.mark.parametrize('browser', DRIVER_PORTS.keys(), scope="class")
@pytest.mark.parametrize('browser', BROWSERS, scope="class")
class TestDrivers:
"""Simple tests checking our setup for Selenium.
"""
......
......@@ -7,6 +7,7 @@ import pytest
import nsfarm.board
import nsfarm.cli
import nsfarm.lxd
import nsfarm.web
import nsfarm.target
from . import mark
......@@ -164,6 +165,15 @@ def fixture_lan1_client(lxd, device_map):
yield container
@pytest.fixture(name="lan1_webclient", scope="package")
def fixture_lan1_webclient(lxd, device_map):
"""Starts web-client container on LAN1 and provides it.
"""
with nsfarm.web.Container(lxd, {"net:lan": device_map["net:lan1"]}) as container:
container.shell.run('wait4boot')
yield container
########################################################################################################################
# Standard configuration ###############################################################################################
......
reForis Guide testing
=====================
These tests go trough reForis Guide in a multiple way. They mainly check reForis
stability and ensure that user's experience with first setup is as good as
possible.
This does not test system itself that much. The other tests should ensure that
configuration set by guide is actually fully functional.
import nsfarm.web
import pytest
@pytest.fixture(name="webdriver", scope="package", params=nsfarm.web.BROWSERS)
def fixture_webdriver(client_board, lan1_webclient, request):
"""Provides access to Selenium's web driver."""
with lan1_webclient.webdriver(request.param) as driver:
yield driver
@pytest.fixture(autouse=True)
def fixture_fail_screenshot(request, webdriver, screenshot):
"""Takes screenshot on test failure"""
failed_before = request.session.testsfailed
yield
if failed_before != request.session.testsfailed:
screenshot(webdriver, "fail", "Screenshot of last state before failure is reported")
@pytest.fixture(autouse=True)
def fixture_reset_guide(client_board):
"""Reverts guide setting to the original values (thus to no guide)"""
yield
# Note: wizard might not exist so we mask intentionally here error when it is missing
client_board.run("uci del foris.wizard; uci commit foris")
"""Test DNS configuration in guide.
"""
import pytest
from nsfarm.web import reforis
from .test_net import STEP
@pytest.fixture(name="workflow", autouse=True, params=["router", "bridge"])
def fixture_workflow(request, client_board):
"""Select workflow."""
client_board.run(
" && ".join(
[
"uci set foris.wizard=config",
"uci add_list foris.wizard.passed=password",
"uci add_list foris.wizard.passed=profile",
"uci add_list foris.wizard.passed=networks",
f"uci add_list foris.wizard.passed={STEP[request.param]}",
"uci add_list foris.wizard.passed=time",
f"uci set foris.wizard.workflow='{request.param}'",
"uci commit foris",
]
)
)
return request.param
# The revert is performed by fixture reset_guide
def test_index(webdriver, screenshot):
"""Check that new page redirects us to correct DNS configuration."""
webdriver.get("http://192.168.1.1")
reforis.network.DNS(webdriver).verify()
screenshot(webdriver, "index", "Index page when time is passed.")
def test_save_and_next(client_board, webdriver, screenshot, workflow):
"""Just save network configuration and pass to next step."""
guide = reforis.guide.Guide(webdriver)
guide.go()
reforis.network.DNS(webdriver).save.click()
guide.wait4ready()
screenshot(webdriver, f"dns:{workflow}", f"DNS saved for workflow: {workflow}")
guide.notification_close()
client_board.run("uci get foris.wizard.passed")
assert "dns" in client_board.output.split()
guide.next.click()
assert reforis.packages.UpdateSettings(webdriver).verify()
"""Test finished configuration in guide.
"""
import pytest
from nsfarm.web import reforis
from .test_net import STEP
@pytest.fixture(name="workflow", autouse=True, params=["router", "min", "bridge"])
def fixture_workflow(request, client_board):
"""Select workflow.
"""
cmds = [
"uci set foris.wizard=config",
"uci add_list foris.wizard.passed=password",
"uci add_list foris.wizard.passed=profile",
]
if request.param != "min":
cmds += [
"uci add_list foris.wizard.passed=networks",
f"uci add_list foris.wizard.passed={STEP[request.param]}",
"uci add_list foris.wizard.passed=time",
"uci add_list foris.wizard.passed=dns",
"uci add_list foris.wizard.passed=updater",
]
cmds += [
f"uci set foris.wizard.workflow='{request.param}'",
"uci commit foris"
]
client_board.run(" && ".join(cmds))
return request.param
# The revert is performed by fixture reset_guide
def test_index(webdriver, screenshot):
"""Check that new page redirects us to correct DNS configuration.
"""
webdriver.get('http://192.168.1.1')
reforis.guide.Finished(webdriver).verify()
screenshot(webdriver, "index", "Index page when guide is finished.")
def test_finish(client_board, webdriver):
"""Just click continue.
"""
reforis.guide.Guide(webdriver).go()
reforis.guide.Finished(webdriver).cont.click()
assert reforis.Overview(webdriver).verify()
client_board.run("uci get foris.wizard.passed")
assert "finished" in client_board.output.split()
client_board.run("uci get foris.wizard.finished")
assert client_board.output == "1"
"""Test just pass trough interfaces configuration.
"""
import pytest
from nsfarm.web import reforis
from .test_net import NET
@pytest.fixture(name="workflow", autouse=True, params=["router", "bridge"])
def fixture_workflow(request, client_board):
"""Set that we passed workflow selection step in the guide."""
client_board.run(
" && ".join(
[
"uci set foris.wizard=config",
"uci add_list foris.wizard.passed=password",
"uci add_list foris.wizard.passed=profile",
f"uci set foris.wizard.workflow='{request.param}'",
"uci commit foris",
]
)
)
return request.param
# The revert is performed by fixture reset_guide
def test_index(webdriver, screenshot):
"""Check that new page redirects us to workflow."""
webdriver.get("http://192.168.1.1")
assert reforis.network.Interfaces(webdriver).verify()
screenshot(webdriver, "index", "Index page when workflow is passed.")
def test_save_and_next(client_board, webdriver, screenshot, workflow):
"""Just save interfaces configuration and pass to next step."""
guide = reforis.guide.Guide(webdriver)
guide.go()
reforis.network.Interfaces(webdriver).save.click()
guide.wait4ready()
screenshot(webdriver, f"interfaces:{workflow}", f"Interfaces saved for workflow: {workflow}")
client_board.run("uci get foris.wizard.passed")
assert "networks" in client_board.output.split()
guide.next.click()
assert NET[workflow](webdriver).verify()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment