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"