Skip to content

Commit

Permalink
Merge branch 'main' into update-conda-store-config
Browse files Browse the repository at this point in the history
  • Loading branch information
viniciusdc authored Feb 6, 2025
2 parents e7f7d63 + 7e757c5 commit 49abd58
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,16 @@ def get_conda_store_environments(user_info: dict):
# will contain all the environment info returned from the api
env_data = []

# If there are more records than the specified size limit, then
# will need to page through to get all the available envs
if total_records > page_size:
# generate a list of urls to hit to build the response
urls = generate_paged_urls(base_url, total_records, page_size)

# get content from urls
for url in urls:
response = http.request(
"GET", url, headers={"Authorization": f"Bearer {token}"}
)
decoded_response = json.loads(response.data.decode("UTF-8"))
env_data += decoded_response.get("data", [])
# generate a list of urls to hit to build the response
urls = generate_paged_urls(base_url, total_records, page_size)

# get content from urls
for url in urls:
response = http.request(
"GET", url, headers={"Authorization": f"Bearer {token}"}
)
decoded_response = json.loads(response.data.decode("UTF-8"))
env_data += decoded_response.get("data", [])

# Filter and return conda environments for the user
return [f"{env['namespace']['name']}-{env['name']}" for env in env_data]
Expand Down
44 changes: 32 additions & 12 deletions tests/common/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,31 @@ def _dismiss_kernel_popup(self):
def _shutdown_all_kernels(self):
"""Shutdown all running kernels."""
logger.debug(">>> Shutting down all kernels")
kernel_menu = self.page.get_by_role("menuitem", name="Kernel")
kernel_menu.click()

# Open the "Kernel" menu
self.page.get_by_role("menuitem", name="Kernel").click()

# Locate the "Shut Down All Kernels…" menu item
shut_down_all = self.page.get_by_role("menuitem", name="Shut Down All Kernels…")
logger.debug(
f">>> Shut down all kernels visible: {shut_down_all.is_visible()} enabled: {shut_down_all.is_enabled()}"
)
if shut_down_all.is_visible() and shut_down_all.is_enabled():
shut_down_all.click()
self.page.get_by_role("button", name="Shut Down All").click()
else:

# If it's not visible or is disabled, there's nothing to shut down
if not shut_down_all.is_visible() or shut_down_all.is_disabled():
logger.debug(">>> No kernels to shut down")
return

# Otherwise, click to shut down all kernels and confirm
shut_down_all.click()
self.page.get_by_role("button", name="Shut Down All").click()

def _navigate_to_root_folder(self):
"""Navigate back to the root folder in JupyterLab."""
# Make sure the home directory is select in the sidebar
if not self.page.get_by_role(
"region", name="File Browser Section"
).is_visible():
file_browser_tab = self.page.get_by_role("tab", name="File Browser")
file_browser_tab.click()

logger.debug(">>> Navigating to root folder")
self.page.get_by_title(f"/home/{self.nav.username}", exact=True).locator(
"path"
Expand Down Expand Up @@ -303,9 +314,18 @@ def _open_new_environment_tab(self):
).to_be_visible()

def _assert_user_namespace(self):
expect(
self.page.get_by_role("button", name=f"{self.nav.username} Create a new")
).to_be_visible()
user_namespace_dropdown = self.page.get_by_role(
"button", name=f"{self.nav.username} Create a new"
)

if not (
expect(
user_namespace_dropdown
).to_be_visible() # this asserts the user namespace shows in the UI
or self.nav.username
in user_namespace_dropdown.text_content() # this attests that the namespace corresponds to the logged in user
):
raise ValueError(f"User namespace {self.nav.username} not found")

def _get_shown_namespaces(self):
_envs = self.page.locator("#environmentsScroll").get_by_role("button")
Expand Down
18 changes: 13 additions & 5 deletions tests/common/navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path

from playwright.sync_api import expect, sync_playwright
from yarl import URL

logger = logging.getLogger()

Expand Down Expand Up @@ -50,7 +51,7 @@ def setup(self):
self.page = self.context.new_page()
self.initialized = True

def _rename_test_video_path(self, video_path):
def _rename_test_video_path(self, video_path: Path):
"""Rename the test video file to the test unique identifier."""
video_file_name = (
f"{self.video_name_prefix}.mp4" if self.video_name_prefix else None
Expand All @@ -62,7 +63,7 @@ def teardown(self) -> None:
"""Teardown Playwright browser and context."""
if self.initialized:
# Rename the video file to the test unique identifier
current_video_path = self.page.video.path()
current_video_path = Path(self.page.video.path())
self._rename_test_video_path(current_video_path)

self.context.close()
Expand All @@ -87,10 +88,17 @@ class LoginNavigator(NavigatorMixin):

def __init__(self, nebari_url, username, password, auth="password", **kwargs):
super().__init__(**kwargs)
self.nebari_url = nebari_url
self._nebari_url = URL(nebari_url)
self.username = username
self.password = password
self.auth = auth
logger.debug(
f"LoginNavigator initialized with {self.auth} auth method. :: {self.nebari_url}"
)

@property
def nebari_url(self):
return self._nebari_url.human_repr()

def login(self):
"""Login to Nebari deployment using the provided authentication method."""
Expand All @@ -110,7 +118,7 @@ def logout(self):

def _login_google(self):
logger.debug(">>> Sign in via Google and start the server")
self.page.goto(self.nebari_url)
self.page.goto(url=self.nebari_url)
expect(self.page).to_have_url(re.compile(f"{self.nebari_url}*"))

self.page.get_by_role("button", name="Sign in with Keycloak").click()
Expand All @@ -123,7 +131,7 @@ def _login_google(self):

def _login_password(self):
logger.debug(">>> Sign in via Username/Password")
self.page.goto(self.nebari_url)
self.page.goto(url=self.nebari_url)
expect(self.page).to_have_url(re.compile(f"{self.nebari_url}*"))

self.page.get_by_role("button", name="Sign in with Keycloak").click()
Expand Down
32 changes: 29 additions & 3 deletions tests/common/playwright_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,43 @@ def load_env_vars():
def build_params(request, pytestconfig, extra_params=None):
"""Construct and return parameters for navigator instances."""
env_vars = load_env_vars()

# Retrieve values from request or environment
nebari_url = request.param.get("nebari_url") or env_vars.get("nebari_url")
username = request.param.get("keycloak_username") or env_vars.get("username")
password = request.param.get("keycloak_password") or env_vars.get("password")

# Validate that required fields are present
if not nebari_url:
raise ValueError(
"Error: 'nebari_url' is required but was not provided in "
"'request.param' or environment variables."
)
if not username:
raise ValueError(
"Error: 'username' is required but was not provided in "
"'request.param' or environment variables."
)
if not password:
raise ValueError(
"Error: 'password' is required but was not provided in "
"'request.param' or environment variables."
)

# Build the params dictionary once all required fields are validated
params = {
"nebari_url": request.param.get("nebari_url") or env_vars["nebari_url"],
"username": request.param.get("keycloak_username") or env_vars["username"],
"password": request.param.get("keycloak_password") or env_vars["password"],
"nebari_url": nebari_url,
"username": username,
"password": password,
"auth": "password",
"video_dir": "videos/",
"headless": pytestconfig.getoption("--headed"),
"slow_mo": pytestconfig.getoption("--slowmo"),
}

if extra_params:
params.update(extra_params)

return params


Expand Down
2 changes: 1 addition & 1 deletion tests/tests_e2e/playwright/.env.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
KEYCLOAK_USERNAME="USERNAME_OR_GOOGLE_EMAIL"
KEYCLOAK_PASSWORD="PASSWORD"
NEBARI_FULL_URL="https://nebari.quansight.dev/"
NEBARI_FULL_URL="https://localhost/"
10 changes: 10 additions & 0 deletions tests/tests_e2e/playwright/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.PHONY: setup

setup:
@echo "Setting up correct pins for playwright user-journey tests"
pip install -r requirements.txt
@echo "Setting up playwright browser dependencies"
playwright install
@echo "Setting up .env file"
cp .env.tpl .env
@echo "Please fill in the .env file with the correct values"
63 changes: 43 additions & 20 deletions tests/tests_e2e/playwright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,48 +33,57 @@ tests
- `handlers.py`: Contains classes fore handling the different level of access to
services a User might encounter, such as Notebook, Conda-store and others.

## Setup

1. **Install Nebari with Development Requirements**
Below is an example of how you might update the **Setup** and **Running the Playwright Tests** sections of your README to reflect the new `Makefile` and the updated `pytest` invocation.

Install Nebari including development requirements (which include Playwright):
---

```bash
pip install -e ".[dev]"
```
## Setup

2. **Install Playwright**
1. **Use the provided Makefile to install dependencies**

Install Playwright:
Navigate to the Playwright tests directory and run the `setup` target:

```bash
playwright install
cd tests_e2e/playwright
make setup
```

*Note:* If you see the warning `BEWARE: your OS is not officially supported by Playwright; downloading fallback build`, it is not critical. Playwright should still work (see microsoft/playwright#15124).
This command will:

3. **Create Environment Vars**
- Install the pinned dependencies from `requirements.txt`.
- Install Playwright and its required browser dependencies.
- Create a new `.env` file from `.env.tpl`.

Fill in your execution space environment with the following values:
2. **Fill in the `.env` file**

- `KEYCLOAK_USERNAME`: Nebari username for username/password login or Google email address/Google sign-in.
- `KEYCLOAK_PASSWORD`: Password associated with `KEYCLOAK_USERNAME`.
- `NEBARI_FULL_URL`: Full URL path including scheme to the Nebari instance (e.g., "https://nebari.quansight.dev/").
Open the newly created `.env` file and fill in the following values:

This user can be created with the following command (or use an existing non-root user):
- `KEYCLOAK_USERNAME`: Nebari username for username/password login (or Google email for Google sign-in).
- `KEYCLOAK_PASSWORD`: Password associated with the above username.
- `NEBARI_FULL_URL`: Full URL (including `https://`) to the Nebari instance (e.g., `https://nebari.quansight.dev/`).

If you need to create a user for testing, you can do so with:

```bash
nebari keycloak adduser --user <username> <password> --config <NEBARI_CONFIG_PATH>
```

## Running the Playwright Tests
*Note:* If you see the warning:
```
BEWARE: your OS is not officially supported by Playwright; downloading fallback build
```
it is not critical. Playwright should still work despite the warning.

Playwright tests are run inside of pytest using:
## Running the Playwright Tests

You can run the Playwright tests with `pytest`.
```bash
pytest tests_e2e/playwright/test_playwright.py
pytest tests_e2e/playwright/test_playwright.py --numprocesses auto
```

> **Important**: Due to how Pytest manages async code; Playwright’s sync calls can conflict with default Pytest concurrency settings, and using `--numprocesses auto` helps mitigate potential thread-blocking issues.

Videos of the test playback will be available in `$PWD/videos/`. To disabled the browser
runtime preview of what is happening while the test runs, pass the `--headed` option to `pytest`. You
can also add the `--slowmo=$MILLI_SECONDS` option to introduce a delay before each
Expand Down Expand Up @@ -188,3 +197,17 @@ If your test suit presents a need for a more complex sequence of actions or spec
parsing around the contents present in each page, you can create
your own handler to execute the auxiliary actions while the test is running. Check the
`handlers.py` over some examples of how that's being done.


## Debugging Playwright tests

Playwright supports a debug mode called
[Inspector](https://playwright.dev/python/docs/debug#playwright-inspector) that can be
used to inspect the browser and the page while the test is running. To enabled this
debugging option within the tests execution you can pass the `PWDEBUG=1` variable within
your test execution command.

For example, to run a single test with the debug mode enabled, you can use the following
```bash
PWDEBUG=1 pytest -s test_playwright.py::test_notebook --numprocesses 1
```
4 changes: 4 additions & 0 deletions tests/tests_e2e/playwright/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
playwright==1.50.0
pytest==8.0.0
pytest-playwright==0.7.0
pytest-xdist==3.6.1
3 changes: 2 additions & 1 deletion tests/tests_e2e/playwright/test_playwright.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def test_login_logout(navigator):
)
@login_parameterized()
def test_navbar_services(navigator, services):
navigator.page.goto(navigator.nebari_url + "hub/home")
home_url = navigator._nebari_url / "hub/home"
navigator.page.goto(home_url.human_repr())
navigator.page.wait_for_load_state("networkidle")
navbar_items = navigator.page.locator("#thenavbar").get_by_role("link")
navbar_items_names = [item.text_content() for item in navbar_items.all()]
Expand Down

0 comments on commit 49abd58

Please sign in to comment.