diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 170ee9c..ea0413a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: run: echo "JUPYTER_TOKEN=$(openssl rand -hex 32)" >> $GITHUB_ENV - name: Run pytest - run: pytest -v --driver ${{ matrix.browser }} tests_notebooks + run: pytest -v --driver ${{ matrix.browser }} tests_notebooks/ env: TAG: edge diff --git a/home/start_page.py b/home/start_page.py index 69942eb..8c9abeb 100644 --- a/home/start_page.py +++ b/home/start_page.py @@ -3,6 +3,7 @@ import json from glob import glob from os import path +from pathlib import Path import ipywidgets as ipw import traitlets @@ -29,7 +30,7 @@ def create_app_widget_move_buttons(name): class AiidaLabHome: - """Class that mananges the appearance of the AiiDAlab home page.""" + """Class that manages the appearance of the AiiDAlab home page.""" def __init__(self): self.config_fn = ".launcher.json" @@ -40,16 +41,16 @@ def _create_app_widget(self, name): """Create the widget representing the app on the home screen.""" config = self.read_config() app = AiidaLabApp(name, None, AIIDALAB_APPS) - - if name == "home": - app_widget = AppWidget(app, allow_move=False, allow_manage=False) - else: - app_widget = CollapsableAppWidget(app, allow_move=True) - app_widget.hidden = name in config["hidden"] - app_widget.observe(self._on_app_widget_change_hidden, names=["hidden"]) - + app_widget = CollapsableAppWidget(app, allow_move=True) + app_widget.hidden = name in config["hidden"] + app_widget.observe(self._on_app_widget_change_hidden, names=["hidden"]) return app_widget + def _create_home_widget(self): + """Create the home app widget.""" + app = AiidaLabApp("home", None, AIIDALAB_APPS) + return AppWidget(app, allow_move=False, allow_manage=False) + def _on_app_widget_change_hidden(self, change): """Record whether a app widget is hidden on the home screen in the config file.""" config = self.read_config() @@ -72,7 +73,17 @@ def read_config(self): def render(self): """Rendering all apps.""" - displayed_apps = [] + home = self._create_home_widget() + children = [home] + + config_dir = Path.home() / ".aiidalab" + warning_file = config_dir / "home_app_warning.md" + + if warning_file.exists(): + content = warning_file.read_text() + notification = self._create_notification(content) + children.append(notification) + apps = self.load_apps() for name in apps: @@ -80,8 +91,10 @@ def render(self): if name not in self._app_widgets: self._app_widgets[name] = self._create_app_widget(name) - displayed_apps.append(self._app_widgets[name]) - self.output.children = displayed_apps + children.append(self._app_widgets[name]) + + self.output.children = children + return self.output def load_apps(self): @@ -98,7 +111,7 @@ def load_apps(self): apps.sort(key=lambda x: order.index(x) if x in order else -1) config["order"] = apps self.write_config(config) - return ["home", *apps] + return apps def move_updown(self, name, delta): """Move the app up/down on the start page.""" @@ -111,6 +124,19 @@ def move_updown(self, name, delta): config["order"] = order self.write_config(config) + def _create_notification(self, content): + from IPython.display import Markdown, display + from jinja2 import Environment + + env = Environment() + notification = env.from_string(content).render() + output = ipw.Output() + notification_widget = ipw.VBox(children=[output]) + notification_widget.add_class("home-notification") + with output: + display(Markdown(notification)) + return notification_widget + class AppWidget(ipw.VBox): """Widget that represents an app as part of the home page.""" @@ -119,7 +145,7 @@ def __init__(self, app, allow_move=False, allow_manage=True): self.app = app launcher = load_widget(app.name) - launcher.layout = ipw.Layout(width="900px") + launcher.layout.flex = "1" # fill available space header_items = [] footer_items = [] @@ -128,7 +154,7 @@ def __init__(self, app, allow_move=False, allow_manage=True): app_status_info = AppStatusInfoWidget() for trait in ("detached", "compatible", "remote_update_status"): ipw.dlink((app, trait), (app_status_info, trait)) - app_status_info.layout.margin = "0px 0px 0px 800px" + app_status_info.layout.margin = "0px 0px 0px auto" header_items.append(app_status_info) footer_items.append( @@ -150,7 +176,7 @@ def __init__(self, app, allow_move=False, allow_manage=True): footer = ipw.HTML(" ".join(footer_items), layout={"width": "initial"}) footer.layout.margin = ( - "0px 0px 0px 700px" if allow_manage else "0px 0px 20px 0px" + "0px 0px 0px auto" if allow_manage else "0px 0px 20px 0px" ) super().__init__(children=[header, body, footer]) diff --git a/start.ipynb b/start.ipynb index fc51cd7..599db74 100644 --- a/start.ipynb +++ b/start.ipynb @@ -31,6 +31,12 @@ " .output_subarea {\n", " max-width: none !important;\n", " }\n", + " .home-notification {\n", + " background-color: antiquewhite;\n", + " margin: 2px;\n", + " padding: 8px;\n", + " border: 1px solid red;\n", + " }\n", "\n" ] }, @@ -64,7 +70,7 @@ " home.move_updown(parsed_url[\"move_up\"][0], -1)\n", "elif \"move_down\" in parsed_url:\n", " home.move_updown(parsed_url[\"move_down\"][0], +1)\n", - "display(home.render())" + "home.render()" ] }, { diff --git a/tests_notebooks/conftest.py b/tests_notebooks/conftest.py index 379aa02..6090c21 100644 --- a/tests_notebooks/conftest.py +++ b/tests_notebooks/conftest.py @@ -41,25 +41,39 @@ def docker_compose(docker_services): @pytest.fixture(scope="session") -def aiidalab_exec(docker_compose): - def execute(command, user=None, **kwargs): - workdir = "/home/jovyan/apps/home" - if user: - command = f"exec --workdir {workdir} -T --user={user} aiidalab {command}" - else: - command = f"exec --workdir {workdir} -T aiidalab {command}" +def aiidalab_exec(docker_compose, nb_user): + """Execute command inside the AiiDAlab test container""" + def execute(command, user=None, **kwargs): + workdir = f"/home/{nb_user}/apps/home" + if user is None: + user = nb_user + command = ( + f"exec --workdir {workdir} -T --user={user} aiidalab bash -c '{command}'" + ) return docker_compose.execute(command, **kwargs) return execute +@pytest.fixture(scope="session") +def nb_user(): + return "jovyan" + + +@pytest.fixture +def create_warning_file(nb_user, aiidalab_exec): + config_folder = f"/home/{nb_user}/.aiidalab" + aiidalab_exec(f"mkdir -p {config_folder}") + aiidalab_exec(f"echo Warning! > {config_folder}/home_app_warning.md") + + @pytest.fixture(scope="session", autouse=True) -def notebook_service(docker_ip, docker_services, aiidalab_exec): +def notebook_service(docker_ip, docker_services, aiidalab_exec, nb_user): """Ensure that HTTP service is up and responsive.""" # Directory ~/apps/home/ is mounted by docker, # make it writeable for jovyan user, needed for `pip install` - aiidalab_exec("chmod -R a+rw /home/jovyan/apps/home", user="root") + aiidalab_exec(f"chmod -R a+rw /home/{nb_user}/apps/home", user="root") aiidalab_exec("pip install --no-cache-dir .") @@ -75,6 +89,12 @@ def notebook_service(docker_ip, docker_services, aiidalab_exec): @pytest.fixture(scope="function") def selenium_driver(selenium, notebook_service): + """This is the main fixture to be used in tests. + + We're already guaranteed that the container is up and responding to HTTP requests. + (via `notebook_service` fixture). + """ + def _selenium_driver(nb_path, url_params=None): url, token = notebook_service url_with_token = urljoin(url, f"apps/apps/home/{nb_path}?token={token}") @@ -109,8 +129,9 @@ def _selenium_driver(nb_path, url_params=None): @pytest.fixture def final_screenshot(request, screenshot_dir, selenium): """Take screenshot at the end of the test. + Screenshot name is generated from the test function name - by stripping the 'test_' prefix + by stripping the 'test_' prefix. """ screenshot_name = f"{request.function.__name__[5:]}.png" screenshot_path = Path.joinpath(screenshot_dir, screenshot_name) diff --git a/tests_notebooks/test_notification.py b/tests_notebooks/test_notification.py new file mode 100644 index 0000000..5d0004d --- /dev/null +++ b/tests_notebooks/test_notification.py @@ -0,0 +1,11 @@ +from selenium.webdriver.common.by import By + + +def test_home_notification(selenium_driver, create_warning_file, final_screenshot): + selenium = selenium_driver("start.ipynb") + selenium.set_window_size(1000, 941) + notifications = selenium.find_elements(By.CLASS_NAME, "home-notification") + assert len(notifications) == 1 + home_warning = notifications[0] + content_element = home_warning.find_element(By.TAG_NAME, "p") + assert content_element.text == "Warning!"