diff --git a/.github/workflows/toolshed.yaml b/.github/workflows/toolshed.yaml
index acb57d3471c4..f28bc8a12598 100644
--- a/.github/workflows/toolshed.yaml
+++ b/.github/workflows/toolshed.yaml
@@ -22,7 +22,12 @@ jobs:
matrix:
python-version: ['3.7']
test-install-client: ['standalone', 'galaxy_api']
- shed-api: ['v1', 'v2']
+ # v1 is mostly working...
+ shed-api: ['v1']
+ # lets get twill working with twill then try to
+ # make progress on the playwright
+ # shed-browser: ['twill', 'playwright']
+ shed-browser: ['playwright']
services:
postgres:
image: postgres:13
@@ -54,11 +59,21 @@ jobs:
with:
path: 'galaxy_root/.venv'
key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy_root/requirements.txt') }}-toolshed
+ - name: Install dependencies
+ run: ./scripts/common_startup.sh --skip-client-build
+ working-directory: 'galaxy_root'
+ - name: Build Frontend
+ run: '. .venv/bin/activate && cd lib/tool_shed/webapp/frontend && yarn && make client'
+ working-directory: 'galaxy_root'
+ - name: Install playwright
+ run: '. .venv/bin/activate && playwright install'
+ working-directory: 'galaxy_root'
- name: Run tests
run: './run_tests.sh -toolshed'
env:
TOOL_SHED_TEST_INSTALL_CLIENT: ${{ matrix.test-install-client }}
TOOL_SHED_API_VERSION: ${{ matrix.shed-api }}
+ TOOL_SHED_TEST_BROWSER: ${{ matrix.shed-browser }}
working-directory: 'galaxy_root'
- uses: actions/upload-artifact@v3
if: failure()
diff --git a/lib/galaxy/dependencies/dev-requirements.txt b/lib/galaxy/dependencies/dev-requirements.txt
index c18359a11cc1..f8d303e46d8e 100644
--- a/lib/galaxy/dependencies/dev-requirements.txt
+++ b/lib/galaxy/dependencies/dev-requirements.txt
@@ -88,6 +88,7 @@ pytest-html==3.2.0 ; python_version >= "3.7" and python_version < "3.12"
pytest-json-report==1.5.0 ; python_version >= "3.7" and python_version < "3.12"
pytest-metadata==2.0.4 ; python_version >= "3.7" and python_version < "3.12"
pytest-mock==3.10.0 ; python_version >= "3.7" and python_version < "3.12"
+pytest-playwright==0.3.0 ; python_version >= "3.7" and python_version < "3.12"
pytest-postgresql==4.1.1 ; python_version >= "3.7" and python_version < "3.12"
pytest-shard==0.1.2 ; python_version >= "3.7" and python_version < "3.12"
pytest==7.2.1 ; python_version >= "3.7" and python_version < "3.12"
diff --git a/lib/tool_shed/test/base/playwrightbrowser.py b/lib/tool_shed/test/base/playwrightbrowser.py
new file mode 100644
index 000000000000..d8e21b5c5277
--- /dev/null
+++ b/lib/tool_shed/test/base/playwrightbrowser.py
@@ -0,0 +1,153 @@
+import time
+from typing import List
+
+from playwright.sync_api import (
+ expect,
+ Locator,
+ Page,
+)
+
+from .browser import (
+ FormValueType,
+ ShedBrowser,
+)
+
+
+class PlaywrightShedBrowser(ShedBrowser):
+ _page: Page
+
+ def __init__(self, page: Page):
+ self._page = page
+
+ def visit_url(self, url: str, allowed_codes: List[int]) -> str:
+ response = self._page.goto(url)
+ assert response is not None
+ return_code = response.status
+ assert return_code in allowed_codes, "Invalid HTTP return code {}, allowed codes: {}".format(
+ return_code,
+ ", ".join(str(code) for code in allowed_codes),
+ )
+ return response.url
+
+ def page_content(self) -> str:
+ self._page.wait_for_load_state("networkidle")
+ return self._page.content()
+
+ def check_page_for_string(self, patt: str) -> None:
+ """Looks for 'patt' in the current browser page"""
+ patt = patt.replace("", "").replace("", "")
+ expect(self._page.locator("body")).to_contain_text(patt)
+
+ def check_string_not_in_page(self, patt: str) -> None:
+ patt = patt.replace("", "").replace("", "")
+ expect(self._page.locator("body")).not_to_contain_text(patt)
+
+ def xcheck_page_for_string(self, patt: str) -> None:
+ page = self.page_content()
+ if page.find(patt) == -1:
+ fname = self.write_temp_file(page)
+ errmsg = f"no match to '{patt}'\npage content written to '{fname}'\npage: [[{page}]]"
+ raise AssertionError(errmsg)
+
+ def xcheck_string_not_in_page(self, patt: str) -> None:
+ page = self.page_content()
+ if page.find(patt) != -1:
+ fname = self.write_temp_file(page)
+ errmsg = f"string ({patt}) incorrectly displayed in page.\npage content written to '{fname}'"
+ raise AssertionError(errmsg)
+
+ def write_temp_file(self, content, suffix=".html"):
+ import tempfile
+
+ from galaxy.util import smart_str
+
+ with tempfile.NamedTemporaryFile(suffix=suffix, prefix="twilltestcase-", delete=False) as fh:
+ fh.write(smart_str(content))
+ return fh.name
+
+ def show_forms(self) -> Locator:
+ """Shows form, helpful for debugging new tests"""
+ return self._page.locator("form")
+
+ def submit_form_with_name(self, form_name: str, button="runtool_btn", **kwd):
+ form = self._form_with_name(form_name)
+ self._submit_form(form, button, **kwd)
+
+ def submit_form(self, form_no=-1, button="runtool_btn", form=None, **kwd):
+ """Populates and submits a form from the keyword arguments."""
+ # An HTMLForm contains a sequence of Controls. Supported control classes are:
+ # TextControl, FileControl, ListControl, RadioControl, CheckboxControl, SelectControl,
+ # SubmitControl, ImageControl
+ if form is None:
+ try:
+ form = self.show_forms().nth(form_no)
+ except IndexError:
+ raise ValueError("No form to submit found")
+ self._submit_form(form, button, **kwd)
+
+ def _submit_form(self, form: Locator, button="runtool_btn", **kwd):
+ for control_name, control_value in kwd.items():
+ self._fill_form_value(form, control_name, control_value)
+ input = self._page.locator(f"[name='{button}']")
+ if input.count():
+ input.click()
+ else:
+ submit_input = form.locator("input[type=submit]")
+ submit_input.click()
+ time.sleep(0.25)
+ # tc.submit(button)
+
+ def _form_with_name(self, name: str) -> Locator:
+ forms = self.show_forms()
+ count = forms.count()
+ for i in range(count):
+ nth_form = self.show_forms().nth(i)
+ if nth_form.get_attribute("name") == name:
+ return nth_form
+ raise KeyError(f"No form with name [{name}]")
+
+ def fill_form_value(self, form_name: str, control_name: str, value: FormValueType):
+ form: Locator = self._form_with_name(form_name)
+ self._fill_form_value(form, control_name, value)
+
+ def _fill_form_value(self, form: Locator, control_name: str, value: FormValueType):
+ input_i = form.locator(f"input[name='{control_name}']")
+ input_t = form.locator(f"textarea[name='{control_name}']")
+ input_s = form.locator(f"select[name='{control_name}']")
+ if input_i.count():
+ if control_name in ["redirect"]:
+ input_i.input_value = value
+ else:
+ if type(value) is bool:
+ if value and not input_i.is_checked():
+ input_i.check()
+ elif not value and input_i.is_checked():
+ input_i.uncheck()
+ else:
+ input_i.fill(value)
+ if input_t.count():
+ input_t.fill(value)
+ if input_s.count():
+ input_s.select_option(value)
+
+ def edit_repository_categories(self, categories_to_add: List[str], categories_to_remove: List[str]) -> None:
+ multi_select = "form[name='categories'] select[name='category_id']"
+ select_locator = self._page.locator(multi_select)
+ select_locator.evaluate("node => node.selectedOptions = []")
+ select_locator.select_option(label=categories_to_add)
+ self.submit_form_with_name("categories", "manage_categories_button")
+
+ select_locator.evaluate("node => node.selectedOptions = []")
+ select_locator.select_option(label=categories_to_remove)
+ self.submit_form_with_name("categories", "manage_categories_button")
+
+ def grant_users_access(self, usernames: List[str]):
+ multi_select = "form[name='user_access'] select[name='allow_push']"
+ select_locator = self._page.locator(multi_select)
+ select_locator.evaluate("node => node.selectedOptions = []")
+ select_locator.select_option(label=usernames)
+ self.submit_form_with_name("user_access", "user_access_button")
+
+ @property
+ def is_twill(self) -> bool:
+ return False
diff --git a/lib/tool_shed/test/base/twilltestcase.py b/lib/tool_shed/test/base/twilltestcase.py
index 0d8f2433c4e8..f44ba143d987 100644
--- a/lib/tool_shed/test/base/twilltestcase.py
+++ b/lib/tool_shed/test/base/twilltestcase.py
@@ -239,14 +239,18 @@ def display_installed_jobs_list_page(
for data_manager_name in data_manager_names:
params = {"id": data_managers[data_manager_name]["guid"]}
self._visit_galaxy_url("/data_manager/jobs_list", params=params)
- self.testcase.check_for_strings(strings_displayed)
+ content = page_content()
+ for expected in strings_displayed:
+ if content.find(expected) == -1:
+ raise AssertionError(f"Failed to find pattern {expected} in {content}")
def installed_repository_extended_info(
self, installed_repository: galaxy_model.ToolShedRepository
) -> Dict[str, Any]:
params = {"id": self.testcase.security.encode_id(installed_repository.id)}
self._visit_galaxy_url("/admin_toolshed/manage_repository_json", params=params)
- return loads(self.testcase.last_page())
+ json = page_content()
+ return loads(json)
def install_repository(
self,
@@ -654,6 +658,12 @@ def _browser(self) -> ShedBrowser:
assert self.__browser
return self.__browser
+ def _escape_page_content_if_needed(self, content: str) -> str:
+ # if twill browser is being used - replace spaces with " "
+ if self._browser.is_twill:
+ content = content.replace(" ", " ")
+ return content
+
def check_for_strings(self, strings_displayed=None, strings_not_displayed=None):
strings_displayed = strings_displayed or []
strings_not_displayed = strings_not_displayed or []
diff --git a/lib/tool_shed/test/functional/conftest.py b/lib/tool_shed/test/functional/conftest.py
new file mode 100644
index 000000000000..9798b868a212
--- /dev/null
+++ b/lib/tool_shed/test/functional/conftest.py
@@ -0,0 +1,46 @@
+import os
+from typing import (
+ Any,
+ Dict,
+ Generator,
+)
+
+import pytest
+from playwright.sync_api import (
+ Browser,
+ BrowserContext,
+)
+from typing_extensions import Literal
+
+from ..base.browser import ShedBrowser
+from ..base.playwrightbrowser import PlaywrightShedBrowser
+from ..base.twillbrowser import TwillShedBrowser
+
+DEFAULT_BROWSER: Literal["twill", "playwright"] = "playwright"
+
+
+def twill_browser() -> Generator[ShedBrowser, None, None]:
+ yield TwillShedBrowser()
+
+
+def playwright_browser(class_context: BrowserContext) -> Generator[ShedBrowser, None, None]:
+ page = class_context.new_page()
+ yield PlaywrightShedBrowser(page)
+
+
+if os.environ.get("TOOL_SHED_TEST_BROWSER", DEFAULT_BROWSER) == "twill":
+ shed_browser = pytest.fixture(scope="class")(twill_browser)
+else:
+ shed_browser = pytest.fixture(scope="class")(playwright_browser)
+
+
+@pytest.fixture(scope="class")
+def class_context(
+ browser: Browser,
+ browser_context_args: Dict,
+ pytestconfig: Any,
+ request: pytest.FixtureRequest,
+) -> Generator[BrowserContext, None, None]:
+ from pytest_playwright.pytest_playwright import context
+
+ yield from context.__pytest_wrapped__.obj(browser, browser_context_args, pytestconfig, request)
diff --git a/lib/tool_shed/test/functional/test_0000_basic_repository_features.py b/lib/tool_shed/test/functional/test_0000_basic_repository_features.py
index 029f2987866c..bb34b1aca3fb 100644
--- a/lib/tool_shed/test/functional/test_0000_basic_repository_features.py
+++ b/lib/tool_shed/test/functional/test_0000_basic_repository_features.py
@@ -108,10 +108,16 @@ def test_0040_verify_repository(self):
self.browse_repository(
repository, strings_displayed=[f"Repository '{repository.name}' revision", "(repository tip)"]
)
+ strings = ["Uploaded filtering 1.1.0"]
+ if self._browser.is_twill:
+ # this appears in a link - it isn't how one would check this
+ # in playwright. But also we're testing the mercurial page
+ # here so this is probably a questionable check overall.
+ strings += [latest_changeset_revision]
self.display_repository_clone_page(
common.test_user_1_name,
repository_name,
- strings_displayed=["Uploaded filtering 1.1.0", latest_changeset_revision],
+ strings_displayed=strings,
)
def test_0045_alter_repository_states(self):
@@ -145,33 +151,40 @@ def test_0050_display_repository_tip_file(self):
"""Display the contents of filtering.xml in the repository tip revision"""
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
assert repository
- self.display_repository_file_contents(
- repository=repository,
- filename="filtering.xml",
- filepath=None,
- strings_displayed=["1.1.0"],
- strings_not_displayed=[],
- )
+ if self._browser.is_twill:
+ # probably not porting this functionality - just test
+ # with Twill for older UI and drop when that is all dropped
+ self.display_repository_file_contents(
+ repository=repository,
+ filename="filtering.xml",
+ filepath=None,
+ strings_displayed=["1.1.0"],
+ strings_not_displayed=[],
+ )
def test_0055_upload_filtering_txt_file(self):
"""Upload filtering.txt file associated with tool version 1.1.0."""
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
self.add_file_to_repository(repository, "filtering/filtering_0000.txt")
+ expected = self._escape_page_content_if_needed("Readme file for filtering 1.1.0")
self.display_manage_repository_page(
- repository, strings_displayed=["Readme file for filtering 1.1.0"]
+ repository, strings_displayed=[expected]
)
def test_0060_upload_filtering_test_data(self):
"""Upload filtering test data."""
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
self.add_tar_to_repository(repository, "filtering/filtering_test_data.tar")
- self.display_repository_file_contents(
- repository=repository,
- filename="1.bed",
- filepath="test-data",
- strings_displayed=[],
- strings_not_displayed=[],
- )
+ if self._browser.is_twill:
+ # probably not porting this functionality - just test
+ # with Twill for older UI and drop when that is all dropped
+ self.display_repository_file_contents(
+ repository=repository,
+ filename="1.bed",
+ filepath="test-data",
+ strings_displayed=[],
+ strings_not_displayed=[],
+ )
self.check_repository_metadata(repository, tip_only=True)
def test_0065_upload_filtering_2_2_0(self):
@@ -208,15 +221,17 @@ def test_0075_upload_readme_txt_file(self):
"""Upload readme.txt file associated with tool version 2.2.0."""
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
self.add_file_to_repository(repository, "readme.txt")
+ content = self._escape_page_content_if_needed("This is a readme file.")
self.display_manage_repository_page(
- repository, strings_displayed=["This is a readme file."]
+ repository, strings_displayed=[content]
)
# Verify that there is a different readme file for each metadata revision.
+ readme_content = self._escape_page_content_if_needed("Readme file for filtering 1.1.0")
self.display_manage_repository_page(
repository,
strings_displayed=[
- "Readme file for filtering 1.1.0",
- "This is a readme file.",
+ readme_content,
+ content,
],
)
@@ -225,8 +240,9 @@ def test_0080_delete_readme_txt_file(self):
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
self.delete_files_from_repository(repository, filenames=["readme.txt"])
self.check_count_of_metadata_revisions_associated_with_repository(repository, metadata_count=2)
+ readme_content = self._escape_page_content_if_needed("Readme file for filtering 1.1.0")
self.display_manage_repository_page(
- repository, strings_displayed=["Readme file for filtering 1.1.0"]
+ repository, strings_displayed=[readme_content]
)
def test_0085_search_for_valid_filter_tool(self):
@@ -278,11 +294,14 @@ def test_0110_delete_filtering_repository(self):
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
self.login(email=common.admin_email, username=common.admin_username)
self.delete_repository(repository)
+ metadata = self._populator.get_metadata(repository, downloadable_only=False)
+ for _, value in metadata.__root__.items():
+ assert not value.downloadable
# Explicitly reload all metadata revisions from the database, to ensure that we have the current status of the downloadable flag.
# for metadata_revision in repository.metadata_revisions:
# self.test_db_util.refresh(metadata_revision)
# Marking a repository as deleted should result in no metadata revisions being downloadable.
- assert True not in [metadata.downloadable for metadata in self._db_repository(repository).metadata_revisions]
+ # assert True not in [metadata.downloadable for metadata in self._db_repository(repository).metadata_revisions]
def test_0115_undelete_filtering_repository(self):
"""Undelete the filtering_0000 repository and verify that it now has two downloadable revisions."""
@@ -312,8 +331,9 @@ def test_0125_upload_new_readme_file(self):
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
# Upload readme.txt to the filtering_0000 repository and verify that it is now displayed.
self.add_file_to_repository(repository, "filtering/readme.txt")
+ content = self._escape_page_content_if_needed("These characters should not")
self.display_manage_repository_page(
- repository, strings_displayed=["These characters should not"]
+ repository, strings_displayed=[content]
)
def test_0130_verify_handling_of_invalid_characters(self):
@@ -331,13 +351,14 @@ def test_0130_verify_handling_of_invalid_characters(self):
break
# Check for the changeset revision, repository name, owner username, 'repos' in the clone url, and the captured
# unicode decoding error message.
+ content = self._escape_page_content_if_needed("These characters should not")
strings_displayed = [
"%d:%s" % (revision_number, revision_hash),
"filtering_0000",
"user1",
"repos",
"added:",
- "+These characters should not",
+ f"+{content}",
]
self.load_changeset_in_tool_shed(repository_id, changeset_revision, strings_displayed=strings_displayed)
@@ -352,9 +373,11 @@ def test_0140_view_invalid_changeset(self):
"""View repository using an invalid changeset"""
repository = self._get_repository_by_name_and_owner(repository_name, common.test_user_1_name)
encoded_repository_id = repository.id
+ assert encoded_repository_id
strings_displayed = ["Invalid+changeset+revision"]
view_repo_url = (
f"/repository/view_repository?id={encoded_repository_id}&changeset_revision=nonsensical_changeset"
)
self.visit_url(view_repo_url)
- self.check_for_strings(strings_displayed=strings_displayed, strings_not_displayed=[])
+ if self._browser.is_twill:
+ self.check_for_strings(strings_displayed=strings_displayed, strings_not_displayed=[])
diff --git a/pyproject.toml b/pyproject.toml
index e2c036be036a..9465381b5cad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -136,6 +136,7 @@ pytest-html = "*"
python-irodsclient = "!=1.1.2" # https://github.com/irods/python-irodsclient/issues/356
pytest-json-report = "*"
pytest-mock = "*"
+pytest-playwright = "*"
pytest-postgresql = "!=3.0.0" # https://github.com/ClearcodeHQ/pytest-postgresql/issues/426
pytest-shard = "*"
recommonmark = "*"