diff --git a/.env.TEMPLATE b/.env.TEMPLATE deleted file mode 100644 index 533952659..000000000 --- a/.env.TEMPLATE +++ /dev/null @@ -1,7 +0,0 @@ -JENKINS_USERNAME= -JENKINS_PASSWORD= -JENKINS_HOST=localhost -JENKINS_PORT=8080 -BROWSER_NAME=chrome -BROWSER_OPTIONS_CHROME=--window-size=1920,1080 -BROWSER_OPTIONS_EDGE=--window-size=1920,1080 \ No newline at end of file diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 9b8edddf4..6efcd54f6 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -1,6 +1,9 @@ name: Run the tests on pull requests on: + push: + branches: + - main pull_request: branches: - main @@ -43,6 +46,9 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt + - name: Install pytest-repeat + run: pip install pytest-repeat + - name: Install the code linting and formatting tool Ruff run: pip install ruff @@ -69,8 +75,11 @@ jobs: - name: Wait for the Jenkins run: ./.github/wait-for-jenkins.sh - - name: pytest run all tests - run: pytest --alluredir=build/allure-results +# - name: Run tests from tests/api/tests_ui/frees only +# run: pytest tests/api/tests_ui --alluredir=build/allure-results + + - name: Run one test 10 times + run: pytest tests/api/tests_ui/freestyle/test_build_triggers.py --count=1 --alluredir=build/allure-results - name: Get Allure history uses: actions/checkout@v4.2.2 diff --git a/pages/freestyle_project_page.py b/pages/freestyle_project_page.py index eab97f6ef..aa1654295 100644 --- a/pages/freestyle_project_page.py +++ b/pages/freestyle_project_page.py @@ -1,3 +1,5 @@ +import time + import allure import logging @@ -18,7 +20,7 @@ class Locators: CONFIGURE_MENU_ITEM = (By.LINK_TEXT, 'Configure') DESCRIPTION = (By.ID, 'description') MENU_ITEMS = (By.XPATH, '//div[@class="task "]') - BUILDS_LINK = (By.CSS_SELECTOR, "#jenkins-build-history>div>span~div a") + BUILDS_LINK = (By.CSS_SELECTOR, ".app-builds-container__item") def __init__(self, driver, project_name, timeout=5): super().__init__(driver, timeout=timeout) @@ -65,12 +67,11 @@ def get_menu_items_texts(self): return [item.text for item in self.wait_to_be_visible_all(self.Locators.MENU_ITEMS)] @allure.step("Wait up to {timeout} seconds for the build to appear in the build history.") - def wait_for_build_execution(self, timeout): - with allure.step("Wait for 'Builds' link to be visible"): - build_link = self.wait_for_element(self.Locators.BUILDS_LINK, timeout) - if not build_link: - logger.error(f"'Builds' link was not found within {timeout} seconds.") - else: - logger.info(f"'Builds' link appeared within {timeout} seconds.") + def wait_for_build_execution(self, timeout=60): + logger.info("Waiting 60 seconds before counting builds...") + time.sleep(180) + builds = self.find_elements(*self.Locators.BUILDS_LINK) + count = len(builds) + logger.info(f"{count} build(s) appeared after {timeout} seconds.") return self diff --git a/pages/main_page.py b/pages/main_page.py index fb1a9ceec..3b993f9e4 100644 --- a/pages/main_page.py +++ b/pages/main_page.py @@ -94,10 +94,9 @@ def go_to_the_folder_page(self, name): return self.navigate_to(FolderPage, self.Locators.table_item_link(name), name) @allure.step("Go to the Freestyle project page by clicking project link.") - def go_to_freestyle_project_page(self, project_name): + def go_to_freestyle_project_page(self, name): from pages.freestyle_project_page import FreestyleProjectPage - self.wait_to_be_clickable(self.Locators.PROJECT_BUTTON).click() - return FreestyleProjectPage(self.driver, project_name).wait_for_url() + return self.navigate_to(FreestyleProjectPage, self.Locators.table_item_link(name), name) @allure.step("Expand build queue info block if it is collapsed.") def show_build_queue_info_block(self): diff --git a/tests/api/steps/jenkins_steps.py b/tests/api/steps/jenkins_steps.py index 9bdabe12c..5add3653a 100644 --- a/tests/api/steps/jenkins_steps.py +++ b/tests/api/steps/jenkins_steps.py @@ -40,3 +40,23 @@ def post_create_item_in_folder(self, folder_name: str, project_name: str, config f"Endpoint: {endpoint}\n" f"Status: {getattr(response, 'status_code', 'N/A')}" ) + + @allure.step("GET /job/{project_name}/api/json") + def get_job_config_json(self, project_name: str, params: dict = None) -> dict: + endpoint = f"job/{project_name}/api/json" + query_params = {"pretty": "true", **(params or {})} + response = self.client.get(endpoint, params=query_params) + + if response and response.ok: + allure.attach(response.text, name="JSON data", attachment_type=allure.attachment_type.JSON) + return response.json() + + else: + msg = response.text if response else "No response" + allure.attach(msg, name=f"[Failed]{project_name}", attachment_type=allure.attachment_type.TEXT) + raise RuntimeError( + f"Failed to get config for job '{project_name}'.\n" + f"Endpoint: /{endpoint}\n" + f"Params: {params}\n" + f"Status: {response.status_code} - {response.reason}" + ) diff --git a/tests/api/support/jenkins_client.py b/tests/api/support/jenkins_client.py index 3284b445a..9386d9bb1 100644 --- a/tests/api/support/jenkins_client.py +++ b/tests/api/support/jenkins_client.py @@ -27,3 +27,11 @@ def post_xml(self, endpoint: str, xml_data: str): return response return response + + def get(self, endpoint: str, params: dict = None) -> requests.Response: + headers = {'Content-Type': 'application/json'} + headers.update(self.crumb_headers) + url = f"{self.BASE_URL}/{endpoint}" + response = self.session.get(url, headers=headers, params=params) + logger.info(f"[GET] /{endpoint} - Status: {response.status_code} - {response.reason}") + return response diff --git a/tests/api/tests_ui/freestyle/conftest.py b/tests/api/tests_ui/freestyle/conftest.py index 31bf29738..e9c5acad2 100644 --- a/tests/api/tests_ui/freestyle/conftest.py +++ b/tests/api/tests_ui/freestyle/conftest.py @@ -67,3 +67,13 @@ def create_empty_job_with_api() -> Dict[str, Any]: "token": token, "crumb_headers": crumb_headers } + + +@pytest.fixture(scope="function") +def create_freestyle_scheduled_project_by_xml_via_api(jenkins_steps, main_page): + project_name, timer, timeout, config_xml = Data.get_freestyle_scheduled_every_minute_data() + jenkins_steps.post_create_item(project_name, config_xml) + main_page.driver.refresh() + main_page.go_to_freestyle_project_page(project_name).wait_for_build_execution(timeout) + json_data = jenkins_steps.get_job_config_json(project_name) + return project_name, json_data diff --git a/tests/api/tests_ui/freestyle/data.py b/tests/api/tests_ui/freestyle/data.py index 6a1ab754d..0f7c739b2 100644 --- a/tests/api/tests_ui/freestyle/data.py +++ b/tests/api/tests_ui/freestyle/data.py @@ -7,7 +7,7 @@ class CronTimer: every_minute: dict[str, str | int] = { "timer": "*/1 * * * *", "schedule": "1 min", - "timeout": 60 + "timeout": 180 } every_two_minutes: dict[str, str | int] = { "timer": "H/2 * * * *", @@ -31,12 +31,18 @@ def get_freestyle_scheduled_xml(cls, description: str, timer: str) -> str: false false - - {timer} - + + {timer} + false - + + + + sleep 35 + + + diff --git a/tests/api/tests_ui/freestyle/test_build_triggers.py b/tests/api/tests_ui/freestyle/test_build_triggers.py index 6b24362f1..afdd00f5d 100644 --- a/tests/api/tests_ui/freestyle/test_build_triggers.py +++ b/tests/api/tests_ui/freestyle/test_build_triggers.py @@ -1,5 +1,4 @@ import allure -import pytest @allure.epic("Freestyle Project Configuration") @@ -9,7 +8,6 @@ "and that the build runs according to the specified schedule.") @allure.testcase("https://github.com/RedRoverSchool/JenkinsQA_Python_2025_spring/issues/643", "TC_02.004.002") @allure.link("https://github.com/RedRoverSchool/JenkinsQA_Python_2025_spring/issues/643", name="Github issue") -@pytest.mark.xfail(reason="May fail due to non-reproducible concurrent builds locally.") def test_user_can_trigger_build_periodically(create_freestyle_project_scheduled_every_minute_by_api, main_page): project_name, timeout = create_freestyle_project_scheduled_every_minute_by_api @@ -20,9 +18,31 @@ def test_user_can_trigger_build_periodically(create_freestyle_project_scheduled_ .go_to_build_history_page()\ .get_builds_list() + with allure.step("Attach screenshot before asserting number of builds."): + screenshot = main_page.driver.get_screenshot_as_png() + allure.attach(screenshot, name="builds_list_screenshot", attachment_type=allure.attachment_type.PNG) with allure.step("Assert that only one build is displayed in the list."): assert len(builds) == 1, f"Expected 1 build, found {len(builds)}" with allure.step(f"Assert that the project name of the displayed build is \"{project_name}\"."): assert builds[0].split("\n")[0] == project_name, f"No build entry found for '{project_name}'" with allure.step("Assert that the build has number \"#1\"."): assert builds[0].split("\n")[1] == "#1", "Build #1 not found." + + +def test_freestyle_project_ui_fields_match_api_json(create_freestyle_scheduled_project_by_xml_via_api, main_page): + project_name, json_data = create_freestyle_scheduled_project_by_xml_via_api + page = main_page.go_to_freestyle_project_page(project_name) + assert project_name in json_data.get("url") + assert page.get_h1_value() == json_data.get("displayName") + assert page.get_description() == json_data.get("description") + + builds_ui = page.header.go_to_the_main_page() \ + .go_to_build_history_page() \ + .get_builds_list() + + first_build_ui_number = builds_ui[0].split("\n")[1] + first_build_json_number = f"#{json_data['builds'][0]['number']}" + with allure.step("Attach screenshot before asserting number of builds."): + screenshot = main_page.driver.get_screenshot_as_png() + allure.attach(screenshot, name="builds_list_screenshot", attachment_type=allure.attachment_type.PNG) + assert first_build_ui_number == first_build_json_number == "#1"