Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Для информации (исправлять не нужно) - для разных образов (точнее Dockerfile'ов) можно указывать свои .dockerignore (ссылка)

Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ venv
__pycache__
Dockerfile*
app/playground
tests/selenium
7 changes: 4 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ jobs:

- name: Build docker-compose
run: |
docker compose build
docker compose -f docker-compose.yml -f docker-compose-selenium.yml build

- name: Run docker-compose
run:
APP_CONF=../app_conf/testing.ini docker compose up -d
APP_CONF=../app_conf/testing.ini docker compose -f docker-compose.yml -f docker-compose-selenium.yml up -d

- name: Run tests
run: |
docker ps -a
docker compose logs
docker exec web_speech_trainer-web-1 bash -c 'cd /project/tests && pytest .'
docker exec web_speech_trainer-web-1 bash -c 'cd /project/tests && pytest --ignore=selenium'
docker exec web_speech_trainer-selenium-tests-1 bash -c 'pytest .'
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.venv
venv
.venv
.idea
ssl
__pycache__
Expand Down
22 changes: 22 additions & 0 deletions Dockerfile_selenium
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM selenium/standalone-chrome:137.0-chromedriver-137.0-grid-4.33.0-20250606

WORKDIR /usr/src/project

USER root
RUN apt-get update && \
apt-get install -y python3 python3-pip && \
rm -rf /var/lib/apt/lists/*

COPY tests/requirements.txt requirements.txt
RUN pip install -r requirements.txt

COPY tests/selenium selenium
COPY tests/test_data test_data
COPY tests/simple_phrases_russian.wav simple_phrases_russian.wav

COPY app/config.py app/config.py
COPY app_conf/testing.ini app_conf/testing.ini

ENV PYTHONPATH='/usr/src/project/:/usr/src/project/app/'

# CMD ["pytest", "-s", "."]
17 changes: 0 additions & 17 deletions Dockerfile_test

This file was deleted.

1 change: 1 addition & 0 deletions app/routes/lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def lti():
an empty dictionary with 404 HTTP return code if access was denied.
"""
params = request.form

consumer_key = params.get('oauth_consumer_key', '')
consumer_secret = ConsumersDBManager().get_secret(consumer_key)
request_info = dict(
Expand Down
6 changes: 3 additions & 3 deletions app/static/js/recording.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ let gumStream,
timer;

function startRecording() {
console.log("call startRecording(). Try to call navigator.mediaDevices.getUserMedia")
console.log(navigator)
console.log(navigator.mediaDevices)
console.log("call startRecording(). Try to call navigator.mediaDevices.getUserMedia");
console.log("navigator", navigator);
console.log("navigator.mediaDevices", navigator.mediaDevices);
$("#alert").hide()
$("#record-contain").show();
navigator.mediaDevices.getUserMedia({audio: true, video: false}).then(function (stream) {
Expand Down
12 changes: 12 additions & 0 deletions docker-compose-selenium.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '2'

services:
selenium-tests:
build:
context: .
dockerfile: Dockerfile_selenium
shm_size: 2g
depends_on:
- web
networks:
- default
3 changes: 1 addition & 2 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pytest==8.0.2
selenium==4.16.0
webdriver-manager==4.0.1
selenium==4.33.0
16 changes: 16 additions & 0 deletions tests/selenium/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pytest

from selenium_session import SeleniumSession, ROOT_DIR, chrome_options
from app.config import Config

CONFIG_PATH = f'{ROOT_DIR}/app_conf/testing.ini'
AUDIO_FILE = f"{ROOT_DIR}/simple_phrases_russian.wav"

@pytest.fixture(scope="module")
def selenium_session():
Config.init_config(CONFIG_PATH)
session = SeleniumSession(Config.c, chrome_options(AUDIO_FILE))

yield session

session.end_session()
62 changes: 62 additions & 0 deletions tests/selenium/selenium_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import requests
from time import sleep

from selenium.webdriver import Chrome

HOST = 'http://web:5000'
ROOT_DIR = '/usr/src/project'

from selenium.webdriver.chrome.options import Options

def chrome_options(audio_file=None):
chrome_options = Options()

chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--headless')
chrome_options.add_argument(f'--unsafely-treat-insecure-origin-as-secure={HOST}')

if audio_file is not None:
chrome_options.add_argument("--disable-user-media-security")
chrome_options.add_argument("--use-fake-device-for-media-stream")
chrome_options.add_argument("--use-fake-ui-for-media-stream")
chrome_options.add_argument(f'--use-file-for-fake-audio-capture={audio_file}')

return chrome_options

class SeleniumSession:
def __init__(self, config, chrome_options, requires_init=True):
self.__prepare_session(HOST, config, chrome_options, requires_init)

def __init_driver(self, chrome_options):
self.driver = Chrome(options=chrome_options)
self.session = requests.Session()

sleep(5)

def __registrate(self, config):
self.session.request('POST',f'{self.host}/lti', data={
'lis_person_name_full': config.testing.lis_person_name_full,
'ext_user_username': config.testing.session_id,
'custom_task_id': config.testing.custom_task_id,
'custom_task_description': config.testing.custom_task_description,
'custom_attempt_count': config.testing.custom_attempt_count,
'custom_required_points': config.testing.custom_required_points,
'custom_criteria_pack_id': config.testing.custom_criteria_pack_id,
'roles': config.testing.roles,
'lis_outcome_service_url': config.testing.lis_outcome_service_url,
'lis_result_sourcedid': config.testing.lis_result_source_did,
'oauth_consumer_key': config.testing.oauth_consumer_key,
})

def __prepare_session(self, host, config, chrome_options, requires_init):
self.host = host

self.__init_driver(chrome_options)

if requires_init:
self.driver.get(f'{self.host}/init/')

self.__registrate(config)

def end_session(self):
self.driver.quit()
27 changes: 27 additions & 0 deletions tests/selenium/simple_training.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from time import sleep

from selenium_session import ROOT_DIR
from training_session import Training

PRESENTATION_FILE = f"{ROOT_DIR}/test_data/test_presentation_file_0.pdf"
ESTIMATED_PROCESSING_TIME_IN_SECONDS = 100

class SimpleTraining:
def test_presentation_upload(self, selenium_session):
Training(selenium_session).upload_presentation(PRESENTATION_FILE)

def test_record_preparation(self, selenium_session):
Training(selenium_session).prepare_record()
sleep(5)

def test_button_next(self, selenium_session):
Training(selenium_session).next_slide()
sleep(5)

def test_training_session_end(self, selenium_session):
Training(selenium_session).end_training()
sleep(5)

def test_training_feedback(self, selenium_session):
got_feedback = Training(selenium_session).wait_for_feedback(ESTIMATED_PROCESSING_TIME_IN_SECONDS)
assert got_feedback, f"Проверка тренировки заняла более {ESTIMATED_PROCESSING_TIME_IN_SECONDS} секунд"
4 changes: 4 additions & 0 deletions tests/selenium/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from simple_training import SimpleTraining

class TestMain(SimpleTraining):
pass
70 changes: 0 additions & 70 deletions tests/selenium/test_training.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Организуйте запуск тест-кейсов аналогично примеру - в дальнейшем у нас будут появляться новые тестовые сценарии, для которых потребуется централизованный запуск (хотя сейчас это и будет только один)
  2. Вынесите базовую логику теста в отдельный класс
    • так будет проще реализовывать следующие тесты
    • "TestBasicTraining" по факту уже не "basic", а конкретный TestTraining

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Залил коммит. Запуск тест кейсов как в примере, наверное, характерен для unittest.
Я сделал 3 файла:

  • selenium_session - базовая логика для любых будущих селениум тестов
  • training_session - базовая логика для тестирования тренировок, основанная на selenium_session, возможно избыточно
  • test_simple_training - тест, с которого все началось

Мне кажется, для новых тестов будет проще скопировать test_simple_training и использовать SeleniumSession или TrainingSession

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Просто main.py в случае pytest как будто бы избыточен, я могу ошибаться

Copy link
Collaborator Author

@PeeachPie PeeachPie Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сделал немного мудрее. Теперь Training это просто обертка на SeleniumSession использующая экземпляр последнего. Соотвественно обертки можно менять по ходу тестирования. Более того тесты могут наследоваться, что поможет сформировать четкую иерархию

This file was deleted.

15 changes: 0 additions & 15 deletions tests/selenium/testing.ini

This file was deleted.

53 changes: 53 additions & 0 deletions tests/selenium/training_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from time import sleep

from selenium_session import SeleniumSession

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


class Training:
def __init__(self, selenium: SeleniumSession):
self.selenium = selenium

def upload_presentation(self, presentation_path):
self.selenium.driver.get(f'{self.selenium.host}/upload_presentation/')

file_input = WebDriverWait(self.selenium.driver, 20).until(EC.visibility_of_element_located((By.CSS_SELECTOR, "input[type=file]")))
file_input.send_keys(presentation_path)

WebDriverWait(self.selenium.driver, 5).until(EC.element_to_be_clickable((By.ID, "button-submit"))).click()

def prepare_record(self):
WebDriverWait(self.selenium.driver, 10).until(EC.element_to_be_clickable((By.ID, "record"))).click()

WebDriverWait(self.selenium.driver, 10).until(EC.presence_of_element_located((By.ID, "model-timer")))

WebDriverWait(self.selenium.driver, 10).until(EC.invisibility_of_element((By.ID, "model-timer")))

def next_slide(self):
WebDriverWait(self.selenium.driver, 10).until(EC.element_to_be_clickable((By.ID, "next"))).click()

def end_training(self):
WebDriverWait(self.selenium.driver, 5).until(EC.element_to_be_clickable((By.ID, "done"))).click()

WebDriverWait(self.selenium.driver, 5).until(lambda d : d.switch_to.alert).accept()

def wait_for_feedback(self, seconds):
feedback_flag = False
step_count = 10
step = seconds / step_count

for _ in range(step_count):
self.selenium.driver.refresh()

feedback_elements = self.selenium.driver.find_elements(By.ID, 'feedback')

if feedback_elements and feedback_elements[0].text.startswith('Оценка за тренировку'):
feedback_flag = True
break

sleep(step)

return feedback_flag
Binary file modified tests/simple_phrases_russian.wav
Binary file not shown.
Binary file added tests/simple_phrases_russian_old.wav
Binary file not shown.