diff --git a/.env.example b/.env.example index 69340a3b..9240cc9b 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,17 @@ # This .env file will be used by default by docker-compose.yml +# It is also used to set the environment for running tests. # To get started, copy this file to .env + +# REACT_APP_ environment variables are inlined at build time into the bundled +# JavaScript served to the client. Do not store sensitive info in +# REACT_APP_-prefixed variables. +# +# Note: Potential source of confusion. Think of "REACT_APP_" as a variable +# namespace. For example, REACT_APP_LOGIN_PAGE_URL does not point to a login +# page in this repo, despite the fact that this repo (conda-store-ui) is a React +# app; it points instead to the server (conda-store) login page, just like +# REACT_APP_API_URL points to the server API, which is not contained in this +# repo. REACT_APP_API_URL=http://localhost:8080/conda-store/ REACT_APP_AUTH_METHOD=cookie REACT_APP_LOGIN_PAGE_URL=http://localhost:8080/conda-store/login?next= @@ -8,6 +20,7 @@ REACT_APP_STYLE_TYPE=green-accent REACT_APP_CONTEXT=webapp REACT_APP_SHOW_AUTH_BUTTON=true REACT_APP_LOGOUT_PAGE_URL=http://localhost:8080/conda-store/logout?next=/ + # If you want to use a version other than the pinned conda-store-server version # Set the CONDA_STORE_SERVER_VERSION to the package version that you want # CONDA_STORE_SERVER_VERSION="2023.10.1" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2efdaa7..f0852c2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,21 +1,5 @@ name: Playwright Tests -env: - CONDA_STORE_SERVER_PORT: 8000 - CONDA_STORE_BASE_URL: http://localhost:8000 - CONDA_STORE_AUTH: basic - CONDA_STORE_USERNAME: username - CONDA_STORE_PASSWORD: password - REACT_APP_API_URL: http://localhost:8080/conda-store/ - REACT_APP_AUTH_METHOD: cookie - REACT_APP_LOGIN_PAGE_URL: http://localhost:8080/conda-store/login?next= - REACT_APP_AUTH_TOKEN: - REACT_APP_STYLE_TYPE: green-accent - REACT_APP_CONTEXT: webapp - REACT_APP_SHOW_AUTH_BUTTON: true - REACT_APP_LOGOUT_PAGE_URL: http://localhost:8080/conda-store/logout?next=/ - YARN_ENABLE_IMMUTABLE_INSTALLS: false - on: pull_request: push: @@ -47,6 +31,10 @@ jobs: environment-file: environment_dev.yml auto-activate-base: false + - name: "Set up environment variables" + run: | + cp .env.example .env + - name: "Install Dependencies" run: | sudo apt install wait-for-it -y diff --git a/README.md b/README.md index 077d01ac..182a6ea1 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,74 @@ To create a new version of this package, follow these steps: [^github-activity]: If you wish, use [`github-activity` to generate a Changelog](https://github.com/choldgraf/github-activity), e.g. `github-activity conda-incubator/conda-store-ui --since 2023.9.1 --until 2023.10.1 --auth ` +### Running Tests + +This repo contains two types of tests: Jest and Playwright + +#### Jest tests + +To run the Jest tests, run the following command: + +```sh +yarn test +``` + +#### Playwright tests + +Steps to install and set up: + +1. Create Conda environment + ```sh + conda env create -f environment_dev.yml + ``` +2. Activate Conda environment + ```sh + conda activate cs-ui-dev-env + ``` +3. Install Playwright-usable browser + ```sh + playwright install chromium + ``` +4. Copy environment variables + ```sh + cp .env.example .env + ``` +5. Install JavaScript dependencies + ```sh + yarn install --immutable + ``` +6. Build app + ```sh + yarn build + ``` + +To run the tests, you will need to run commands in two separate terminal windows +or tabs. + +Steps to run: + +1. In the first terminal window/tab, enter the following: + ```sh + conda activate cs-ui-dev-env + yarn start + ``` +2. Wait for server to start (you'll know it's ready when it says "webpack 5.xx.x +compiled successfully"). Open a **new terminal window or tab** and enter the +following: + ```sh + conda activate cs-ui-dev-env + pytest + ``` + +If you need to debug, try replacing the last command with: + +```sh +PWDEBUG=1 pytest +``` + +Note: PW stands for Playwright. `PWDEBUG=1` puts [Playwright in debug +mode](https://playwright.dev/python/docs/debug). + ## Code of Conduct 🤝 To guarantee a welcoming and friendly community, we require all community members to follow our [Code of Conduct](https://github.com/conda-incubator/governance/blob/main/CODE_OF_CONDUCT.md). diff --git a/test/playwright/test_ux.py b/test/playwright/test_ux.py index 8bb88a35..765c7953 100644 --- a/test/playwright/test_ux.py +++ b/test/playwright/test_ux.py @@ -1,7 +1,7 @@ """Test suite for user interactions with the UI. It is designed to run both -inside and outside of pytest to make future development easier. +inside and outside of pytest to make future development easier. """ -import os + import requests import time @@ -14,37 +14,23 @@ expect.set_options(timeout=DEFAULT_TIMEOUT) -CONDA_STORE_SERVER_PORT = os.environ.get( - "CONDA_STORE_SERVER_PORT", f"8080" -) -CONDA_STORE_BASE_URL = os.environ.get( - "CONDA_STORE_BASE_URL", f"http://localhost:{CONDA_STORE_SERVER_PORT}" -) -CONDA_STORE_USERNAME = os.environ.get("CONDA_STORE_USERNAME", "username") -CONDA_STORE_PASSWORD = os.environ.get("CONDA_STORE_PASSWORD", "password") - @pytest.fixture def test_config(): - return { - 'base_url': CONDA_STORE_BASE_URL, - 'username': CONDA_STORE_USERNAME, - 'password': CONDA_STORE_PASSWORD, - 'server_port': CONDA_STORE_SERVER_PORT, - } + return {"base_url": "http://localhost:8000"} def _login_sequence(page, screenshot=False): """Conda-store ui login sequence. From the default UI interface, click log - in and go through the log in UI on the following page. The UI will be - returned back to the default UI. + in and go through the log in UI on the following page. The UI will be + returned back to the default UI. Parameters ---------- page: playwright.Page page object for the current test being run screenshot: bool - [Optional] Flag to trigger screenshot collection, set to True to + [Optional] Flag to trigger screenshot collection, set to True to grab screenshots """ # Log in sequence @@ -68,26 +54,26 @@ def _login_sequence(page, screenshot=False): def _create_new_environment(page, screenshot=False): - """Workflow to create a new environment in the UI. The env will be + """Workflow to create a new environment in the UI. The env will be in the "username" workspace and will have a semi-random number to ensure that the env is indeed new since if the environment already exists we get a different UI. This allows this test to be run multiple - times without needing to empty the database. + times without needing to empty the database. Note: this environment takes about a minute to create - WARNING: Changes to this method will require reflective changes on - `_existing_environment_interactions` since it uses this env. - + WARNING: Changes to this method will require reflective changes on + `_existing_environment_interactions` since it uses this env. + Parameters ---------- page: playwright.Page page object for the current test being run screenshot: bool - [Optional] Flag to trigger screenshot collection, set to True to + [Optional] Flag to trigger screenshot collection, set to True to grab screenshots """ # ensure new filename in case this test is run multiple times - new_env_name = f'test_env_{random.randint(0, 100000)}' + new_env_name = f"test_env_{random.randint(0, 100000)}" # set timeout for building the environment time_to_build_env = 5 * 60 * 1000 # 5 minutes in milliseconds @@ -99,7 +85,9 @@ def _create_new_environment(page, screenshot=False): # fill in the env name page.get_by_label("Environment name").fill(new_env_name) # fill in the description - page.get_by_placeholder("Enter here the description of your environment").fill("description") + page.get_by_placeholder("Enter here the description of your environment").fill( + "description" + ) # click the + to add a package page.get_by_role("button", name="+ Add Package").click() # add a package to the ui @@ -115,7 +103,7 @@ def _create_new_environment(page, screenshot=False): page.get_by_label("Enter channel").press("Enter") # click create to start building the env page.get_by_role("button", name="Create", exact=True).click() - + # Interact with the environment shortly after creation # click to open the Active environment dropdown manu page.get_by_text(" - Active", exact=False).click() @@ -125,14 +113,14 @@ def _create_new_environment(page, screenshot=False): expect(page.get_by_text("Building")).to_be_visible() # wait until the status is `Completed` completed = page.get_by_text("Completed", exact=False) - completed.wait_for(state='attached', timeout=time_to_build_env) + completed.wait_for(state="attached", timeout=time_to_build_env) expect(completed).to_be_visible() return new_env_name def _close_environment_tabs(page): - """Close any open tabs in the UI. This will continue closing tabs + """Close any open tabs in the UI. This will continue closing tabs until no tabs remain open. Paramaters @@ -145,13 +133,15 @@ def _close_environment_tabs(page): close_tab.first.click() -def _existing_environment_interactions(page, env_name, time_to_build_env=3*60*1000, screenshot=False): - """test interactions with existing environments. - During this test, the test will be rebuilt twice. +def _existing_environment_interactions( + page, env_name, time_to_build_env=3 * 60 * 1000, screenshot=False +): + """test interactions with existing environments. + During this test, the test will be rebuilt twice. - Note: This test assumes the environment being tested is the one from + Note: This test assumes the environment being tested is the one from `_create_new_environment`. Changes to that method will require changes - here as well (expected existing packages, etc). + here as well (expected existing packages, etc). Parameters ---------- @@ -160,9 +150,9 @@ def _existing_environment_interactions(page, env_name, time_to_build_env=3*60*10 env_name: str Name of existing environment to interact with - must already exist! time_to_build_env: float - [Optional] Time to wait for an updated environment to rebuild in ms + [Optional] Time to wait for an updated environment to rebuild in ms screenshot: bool - [Optional] Flag to trigger screenshot collection, set to True to + [Optional] Flag to trigger screenshot collection, set to True to grab screenshots """ @@ -173,27 +163,37 @@ def _existing_environment_interactions(page, env_name, time_to_build_env=3*60*10 if screenshot: page.screenshot(path="test-results/conda-store-yaml-editor.png") page.get_by_text("- rich").click() - page.get_by_text("channels: - conda-forgedependencies: - rich - pip: - nothing - ipykernel").fill("channels:\n - conda-forge\ndependencies:\n - rich\n - python\n - pip:\n - nothing\n - ipykernel\n\n") + page.get_by_text( + "channels: - conda-forgedependencies: - rich - pip: - nothing - ipykernel" + ).fill( + "channels:\n - conda-forge\ndependencies:\n - rich\n - python\n - pip:\n - nothing\n - ipykernel\n\n" + ) page.get_by_role("button", name="Save").click() # wait until the status is `Completed` completed = page.get_by_text("Completed", exact=False) - completed.wait_for(state='attached', timeout=time_to_build_env) + completed.wait_for(state="attached", timeout=time_to_build_env) # ensure the namespace is expanded - try: + try: expect(page.get_by_role("button", name=env_name)).to_be_visible() except Exception as e: # click to expand the `username` name space (but not click the +) - page.get_by_role("button", name="username Create a new environment in the username namespace").click() + page.get_by_role( + "button", name="username Create a new environment in the username namespace" + ).click() # edit existing environment page.get_by_role("button", name=env_name).click() page.get_by_role("button", name="Edit").click() # page.get_by_placeholder("Enter here the description of your environment").click() # change the description - page.get_by_placeholder("Enter here the description of your environment").fill("new description") + page.get_by_placeholder("Enter here the description of your environment").fill( + "new description" + ) # change the vesion spec of an existing package - page.get_by_role("row", name="ipykernel", exact=False).get_by_role("combobox").first.click() + page.get_by_role("row", name="ipykernel", exact=False).get_by_role( + "combobox" + ).first.click() page.get_by_role("option", name=">=").click() # Note: purposefully not testing version constraint since there is inconsistent behavior here @@ -202,13 +202,17 @@ def _existing_environment_interactions(page, env_name, time_to_build_env=3*60*10 page.get_by_label("Enter package").fill("click") page.get_by_role("option", name="click", exact=True).click() # Note: purposefully not testing version constraint since there is inconsistent behavior here - + # delete a package - page.get_by_role("row", name="rich", exact=False).get_by_test_id("RemovePackageTest").click() + page.get_by_role("row", name="rich", exact=False).get_by_test_id( + "RemovePackageTest" + ).click() # promote a package installed as dependency to specified package - page.locator("#infScroll > .infinite-scroll-component__outerdiv > .infinite-scroll-component > div > div > .MuiButtonBase-root").first.click() - + page.locator( + "#infScroll > .infinite-scroll-component__outerdiv > .infinite-scroll-component > div > div > .MuiButtonBase-root" + ).first.click() + # delete conda-forge channel page.get_by_test_id("DeleteIcon").click() # add conda-forge channel @@ -220,7 +224,7 @@ def _existing_environment_interactions(page, env_name, time_to_build_env=3*60*10 # wait until the status is `Completed` completed = page.get_by_text("Completed", exact=False) - completed.wait_for(state='attached', timeout=time_to_build_env) + completed.wait_for(state="attached", timeout=time_to_build_env) # Edit -> Cancel editing page.get_by_role("button", name=env_name).click() @@ -238,20 +242,20 @@ def _existing_environment_interactions(page, env_name, time_to_build_env=3*60*10 def test_integration(page: Page, test_config, screenshot): """Basic integration test. - When this test runs in CI, we launch the webpack server as a detached - service at the same time that this test is run. For this reason, we + When this test runs in CI, we launch the webpack server as a detached + service at the same time that this test is run. For this reason, we have a try/except here to allow the webpack server to finish deploying - before the test begins. + before the test begins. Parameters ---------- page: playwright.Page page object for the current test being run - test_config: + test_config: Fixture containing the configuration env vars screenshot: bool - Fixture flag to trigger screenshot collection, set to True to - grab screenshots + Fixture flag to trigger screenshot collection, set to True to + grab screenshots """ # wait for server to spin up if necessary server_running = False @@ -260,15 +264,19 @@ def test_integration(page: Page, test_config, screenshot): elapsed_wait_time = 0 # loop until server is running or max_wait_time is reached while not server_running and elapsed_wait_time < max_wait_time: - try: - requests.head(test_config['base_url'], allow_redirects=True).status_code != 200 + try: + requests.head( + test_config["base_url"], allow_redirects=True + ).status_code != 200 server_running = True except requests.exceptions.ConnectionError: elapsed_wait_time += retry_wait_time time.sleep(retry_wait_time) # Go to http://localhost:{server_port} - page.goto(test_config['base_url'], wait_until="domcontentloaded", timeout=4*60*1000) + page.goto( + test_config["base_url"], wait_until="domcontentloaded", timeout=4 * 60 * 1000 + ) page.screenshot(path="test-results/conda-store-unauthenticated.png") if screenshot: @@ -293,10 +301,7 @@ def test_integration(page: Page, test_config, screenshot): """ config = { - 'base_url': f"http://localhost:{CONDA_STORE_SERVER_PORT}", - 'username': CONDA_STORE_USERNAME, - 'password': CONDA_STORE_PASSWORD, - 'server_port': CONDA_STORE_SERVER_PORT, + "base_url": "http://localhost:8000", } screenshot = False @@ -310,8 +315,8 @@ def test_integration(page: Page, test_config, screenshot): page = browser.new_page() # Go to http://localhost:{server_port} - page.goto(config['base_url'], wait_until="domcontentloaded") - + page.goto(config["base_url"], wait_until="domcontentloaded") + # Log in to conda-store _login_sequence(page)