diff --git a/Dockerfile.selenium b/Dockerfile.selenium new file mode 100644 index 0000000..4fc1ca3 --- /dev/null +++ b/Dockerfile.selenium @@ -0,0 +1,11 @@ +FROM python:3.14.3-slim + +WORKDIR /tests + +COPY tests/requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY tests/ . + +ENTRYPOINT [ "pytest", "-v" ] \ No newline at end of file diff --git a/docker-compose.selenium.yml b/docker-compose.selenium.yml new file mode 100644 index 0000000..8f2e4f8 --- /dev/null +++ b/docker-compose.selenium.yml @@ -0,0 +1,36 @@ +services: + web: + build: + context: . + dockerfile: Dockerfile + command: > + sh -c "sed -i 's|let recaptcha_site_key = .*|let recaptcha_site_key = \"${RECAPTCHA_SITE_KEY}\"|' /app/templater/static/js/main.js && + sed -i 's|recaptcha_key = .*|recaptcha_key = \"${RECAPTCHA_SECRET_KEY}\"|' /app/config.ini && + sed -i 's|RECAPTCHA_SITE_KEY|${RECAPTCHA_SITE_KEY}|' /app/templater/templates/layout.jinja2 && + sed -i 's|mongo_uri = .*|mongo_uri = mongodb://db:27017/templater|' /app/config.ini && + sed -i '/def recaptcha/,/return/c\def recaptcha(request):\\n return True' /app/templater/views/default.py && + python3 -u runapp.py" + ports: + - "127.0.0.1:8080:5000" + depends_on: + - db + + db: + image: mongo:8-noble + + tests: + build: + context: . + dockerfile: Dockerfile.selenium + depends_on: + - web + + selenium-hub: + image: selenium/hub:4.41.0-20260222 + ports: + - "127.0.0.1:4445:4444" + + chrome-node: + image: selenium/node-chrome:145.0-20260222 + environment: + - SE_EVENT_BUS_HOST=selenium-hub \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0a16ef2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,27 @@ +import pytest +from selenium import webdriver +from selenium.webdriver.remote.file_detector import LocalFileDetector + +GRID_URL = "http://selenium-hub:4444/wd/hub" +BASE_URL = "http://web:5000" + +@pytest.fixture +def driver(): + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + options.add_argument("--no-sandbox") + options.enable_downloads = True + + driver = webdriver.Remote( + command_executor=GRID_URL, + options=options + ) + driver.file_detector = LocalFileDetector() + + yield driver + + driver.quit() + +@pytest.fixture +def base_url(): + return BASE_URL \ No newline at end of file diff --git a/tests/data/templates/template.docx b/tests/data/templates/template.docx new file mode 100644 index 0000000..e7536bc Binary files /dev/null and b/tests/data/templates/template.docx differ diff --git a/tests/pages/base_page.py b/tests/pages/base_page.py new file mode 100644 index 0000000..87b95b2 --- /dev/null +++ b/tests/pages/base_page.py @@ -0,0 +1,51 @@ +from selenium import webdriver +from selenium.common import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, +) + +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver import ActionChains + +class BasePage: + def __init__(self, driver) -> None: + self.driver = driver + self.wait = WebDriverWait( + driver, + timeout=10, + poll_frequency=2, + ignored_exceptions=[ + NoSuchElementException, + StaleElementReferenceException, + ElementNotInteractableException, + ], + ) + + def element(self, locator: tuple) -> WebElement: + self.wait.until(lambda driver: driver.find_element(*locator)) + return self.driver.find_element(*locator) + + def elements(self, locator: tuple) -> list[WebElement]: + return self.driver.find_elements(*locator) + + def hover(self, locator: tuple) -> None: + element = self.element(locator) + ActionChains(self.driver).move_to_element(element).perform() + + def click(self, locator: tuple) -> None: + element = self.element(locator) + element.click() + + def type(self, locator: tuple, value: str) -> None: + element = self.element(locator) + element.clear() + element.send_keys(value) + + def text(self, locator: tuple) -> str: + element = self.element(locator) + return element.text + + def open(self, url: str) -> None: + self.driver.get(url) diff --git a/tests/pages/upload_page.py b/tests/pages/upload_page.py new file mode 100644 index 0000000..fcdc544 --- /dev/null +++ b/tests/pages/upload_page.py @@ -0,0 +1,36 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC + +from .base_page import BasePage + +class UploadPage(BasePage): + template_input_by = (By.CSS_SELECTOR, "#template-file") + template_upload_btn_by = (By.CSS_SELECTOR, "#btn-upload-template") + + tempate_preview_by = (By.CSS_SELECTOR, "#preview-container") + + template_submit_btn_by = (By.CSS_SELECTOR, "#btn-use-template") + template_reupload_btn_by = (By.CSS_SELECTOR, "#btn-upload-template") + + datatable_tab_pill_by = (By.ID, "pills-datatable-tab") + + def upload_template(self, filepath): + self.element(self.template_input_by).send_keys(filepath) + + def click_upload(self): + self.click(self.template_upload_btn_by) + + def wait_preview(self): + return self.wait.until(EC.visibility_of_element_located(self.tempate_preview_by)) + + def click_use_template(self): + self.click(self.template_submit_btn_by) + + def wait_until_datatable_pill_active(self): + self.wait.until( + EC.text_to_be_present_in_element_attribute( + self.datatable_tab_pill_by, + "aria-selected", + "true" + ) + ) \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..082654e Binary files /dev/null and b/tests/requirements.txt differ diff --git a/tests/test_upload_template.py b/tests/test_upload_template.py new file mode 100644 index 0000000..bb76ed1 --- /dev/null +++ b/tests/test_upload_template.py @@ -0,0 +1,23 @@ +import os +from pages.upload_page import UploadPage +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By + +def test_upload_template(driver, base_url): + page = UploadPage(driver) + + template_path = os.path.abspath("data/templates/template.docx") + print("PATH:", template_path) + + page.open(base_url) + + page.upload_template(template_path) + + page.click_upload() + + preview = page.wait_preview() + assert preview is not None + + page.click_use_template() + + page.wait_until_datatable_pill_active() \ No newline at end of file