From 51123880b9a2b052cea5a7a5628ea61e9e32cb4d Mon Sep 17 00:00:00 2001 From: Radislav Date: Fri, 4 Jul 2025 09:44:15 +0300 Subject: [PATCH] Initial commit --- .env | 3 + .../__pycache__/base_page.cpython-313.pyc | Bin 0 -> 1139 bytes .../configuration_page.cpython-313.pyc | Bin 0 -> 2299 bytes Locators/__pycache__/login.cpython-313.pyc | Bin 0 -> 628 bytes .../__pycache__/main_page.cpython-313.pyc | Bin 0 -> 1121 bytes .../session_locators.cpython-313.pyc | Bin 0 -> 1644 bytes .../__pycache__/users_tab.cpython-313.pyc | Bin 0 -> 1490 bytes Locators/base_page.py | 12 + Locators/configuration_page.py | 20 + Locators/license_tab.py | 5 + Locators/login.py | 4 + Locators/main_page.py | 14 + Locators/session_locators.py | 31 ++ Locators/users_tab.py | 14 + .../conftest.cpython-313-pytest-8.3.5.pyc | Bin 0 -> 470 bytes conftest.py | 7 + data/__pycache__/assertions.cpython-313.pyc | Bin 0 -> 8582 bytes data/__pycache__/constants.cpython-313.pyc | Bin 0 -> 790 bytes data/__pycache__/environment.cpython-313.pyc | Bin 0 -> 1760 bytes data/assertions.py | 107 +++++ data/constants.py | 9 + data/environment.py | 31 ++ .../pages.cpython-313-pytest-8.3.5.pyc | Bin 0 -> 4994 bytes fixtures/pages.py | 95 ++++ fixtures/pages.py.or | 93 ++++ pages/__pycache__/base_page.cpython-313.pyc | Bin 0 -> 16110 bytes .../configuration_page.cpython-313.pyc | Bin 0 -> 3406 bytes pages/__pycache__/login_page.cpython-313.pyc | Bin 0 -> 4120 bytes pages/__pycache__/main_page.cpython-313.pyc | Bin 0 -> 3288 bytes pages/__pycache__/session_tab.cpython-313.pyc | Bin 0 -> 13133 bytes pages/__pycache__/users_tab.cpython-313.pyc | Bin 0 -> 25193 bytes pages/base_page.py | 296 ++++++++++++ pages/configuration_page.py | 49 ++ pages/email_tab.py | 9 + pages/license_tab.py | 102 +++++ pages/login_page.py | 59 +++ pages/main_page.py | 42 ++ pages/service_status_tab.py | 27 ++ pages/session_tab.py | 221 +++++++++ pages/users_tab.py | 432 ++++++++++++++++++ pages/ztp_configuration_tab.py | 9 + pages/ztp_templates_tab.py | 9 + pytest.ini | 4 + requirements.txt | 7 + ...t_session_tab.cpython-313-pytest-8.3.5.pyc | Bin 0 -> 5418 bytes tests/test_license_tab.py | 99 ++++ tests/test_login.py | 23 + tests/test_scroll_tab.py | 54 +++ tests/test_service_status_tab.py | 39 ++ tests/test_session_tab.py | 162 +++++++ tests/test_users_tab.py | 334 ++++++++++++++ 51 files changed, 2422 insertions(+) create mode 100644 .env create mode 100644 Locators/__pycache__/base_page.cpython-313.pyc create mode 100644 Locators/__pycache__/configuration_page.cpython-313.pyc create mode 100644 Locators/__pycache__/login.cpython-313.pyc create mode 100644 Locators/__pycache__/main_page.cpython-313.pyc create mode 100644 Locators/__pycache__/session_locators.cpython-313.pyc create mode 100644 Locators/__pycache__/users_tab.cpython-313.pyc create mode 100644 Locators/base_page.py create mode 100644 Locators/configuration_page.py create mode 100644 Locators/license_tab.py create mode 100644 Locators/login.py create mode 100644 Locators/main_page.py create mode 100644 Locators/session_locators.py create mode 100644 Locators/users_tab.py create mode 100644 __pycache__/conftest.cpython-313-pytest-8.3.5.pyc create mode 100644 conftest.py create mode 100644 data/__pycache__/assertions.cpython-313.pyc create mode 100644 data/__pycache__/constants.cpython-313.pyc create mode 100644 data/__pycache__/environment.cpython-313.pyc create mode 100644 data/assertions.py create mode 100644 data/constants.py create mode 100644 data/environment.py create mode 100644 fixtures/__pycache__/pages.cpython-313-pytest-8.3.5.pyc create mode 100644 fixtures/pages.py create mode 100644 fixtures/pages.py.or create mode 100644 pages/__pycache__/base_page.cpython-313.pyc create mode 100644 pages/__pycache__/configuration_page.cpython-313.pyc create mode 100644 pages/__pycache__/login_page.cpython-313.pyc create mode 100644 pages/__pycache__/main_page.cpython-313.pyc create mode 100644 pages/__pycache__/session_tab.cpython-313.pyc create mode 100644 pages/__pycache__/users_tab.cpython-313.pyc create mode 100644 pages/base_page.py create mode 100644 pages/configuration_page.py create mode 100644 pages/email_tab.py create mode 100644 pages/license_tab.py create mode 100644 pages/login_page.py create mode 100644 pages/main_page.py create mode 100644 pages/service_status_tab.py create mode 100644 pages/session_tab.py create mode 100644 pages/users_tab.py create mode 100644 pages/ztp_configuration_tab.py create mode 100644 pages/ztp_templates_tab.py create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100644 tests/__pycache__/test_session_tab.cpython-313-pytest-8.3.5.pyc create mode 100644 tests/test_license_tab.py create mode 100644 tests/test_login.py create mode 100644 tests/test_scroll_tab.py create mode 100644 tests/test_service_status_tab.py create mode 100644 tests/test_session_tab.py create mode 100644 tests/test_users_tab.py diff --git a/.env b/.env new file mode 100644 index 0000000..73a7825 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +ENV=test +AUTH_LOGIN = admin +AUTH_PASSWORD = admin diff --git a/Locators/__pycache__/base_page.cpython-313.pyc b/Locators/__pycache__/base_page.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..167c119b7e40cf1a7bfc3e825827fb14b3c241b4 GIT binary patch literal 1139 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 a&Vk!g83tn~$n*Taff&EY|IN93kMbY1Wg-Ot literal 0 HcmV?d00001 diff --git a/Locators/__pycache__/users_tab.cpython-313.pyc b/Locators/__pycache__/users_tab.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..495bbaae61a2c33888e5aa323e298ed1d2535f95 GIT binary patch literal 1490 zcmc&!&u<$=6rQ!6G))tJh?E3c6swiGUcg>8P^pRtVq-hi3df7QYg*)8jmGxYS!ul+ zX2)*g3#dpvRSJg|Rfx9Y$j!7uM3jbqVJt4yo+@$T77-+_yj|O^C_x-JFw(qz-}~OY zo%!@;4rXVk5sc4HSsSesLcc^{JOIbzU;#ci(E>V!bo46Hg>#7^K~XCqB^yI9w897+ zoCoJ7G7%MYL=(C|lX`-tU{2^sI;p4V5q*MA>67%ReuPfzQ}mdAl+NhWbhdR&nh9U1 z1~v^0QZM-p!h9P18ddIWf!W$5Y-1&pljYZIo=@Avb$WI-*dz@*!|W}Vk)$;7n(1e` zoU|hHyL^v77+mMy!}lKV53cg>_yfKxT2gJvk}vzN>tAua*77pcCT%+dYl3b|v}$Lq z|MBNbR!@=Trqivx*KkP?q*uglmifM0Clq7mFxM6>86@y(4c}wL@q)}S^G&htH@CA( zqGWZx_^;yK0MWkXeGqPUaCLA!%^$`p?(zG4FU@~COxnRtB<6wB3<|EYdA z+jNNQx3bwMWo3Eet#43SdM=#8IO-0J!)c7$ezOzpBb>qb;|_60%<~vuc4)v{$Fn^j z=$8ylE3GOOTr8*tUacC2Ru1RKL8VZ@RlQgl6LVuCU()pAIQmMQQcw&9U(_n^W2I75 zu%Q~IVz~IE!c*oiYv-P>Rm&e#4ZNmR*2BfaqPU`!iaK5|maCz7XhK;t)HOA)7^+sr zC1tf(8dXyAaVW2q*VM}TkT0l8NxKjp$2eewISou0qfWiU>;Pjr0d08z6a9V*On%vn z_HXjrqZ_&tbygqd54;~m@A5lo{w2S~zc%6S-sX4sjU4|9k{?E0$@d1I@?G%nn-{8T zj(^VYMM3^!eAjp3-uDOBAaTd^+5v_QFuduK?JLx2Z7|$Dk1L8fesauC^af!@>bcEr znuN{8CVGg1H89uEegd7SCU|NdoW+m8IRVbe55ZY@oNn;cYXHSh0G{5TL?_Qd_|X{? z(4*WXo_gc)nTtQCPSd$@;j#p;WCS8bhT-Gjg;VbgTkKu>GQjxvFM_k56a?Xq#H?^h K_zguiUh*fnDi4JK literal 0 HcmV?d00001 diff --git a/Locators/base_page.py b/Locators/base_page.py new file mode 100644 index 0000000..65aec12 --- /dev/null +++ b/Locators/base_page.py @@ -0,0 +1,12 @@ +class BasePageLocators(): + TABLE_HEADERS_CELLS = "//thead[contains(@class,'scrolltable__header')]/tr[contains(@class,'scrolltable__row')]/th" + TABLE_BODY = "//tbody[contains(@class,'scrolltable__body')]/tr[contains(@class,'scrolltable__row')]" + + ALERT_WINDOW = "//div[@role='alert']" + ALERT_WINDOW_TEXT_ERROR = "//div[@class='v-alert error']/div" + ALERT_WINDOW_TEXT_SUCCESS = "//div[@class='v-alert success']/div" + ALERT_WINDOW_TEXT_INFO = "//div[@class='v-alert info']/div" + ALERT_WINDOW_TEXT_WARNING = "//div[@class='v-alert warning']/div" + + TOOLTIP = "//div[contains(@class,'v-tooltip__content menuable__content__active')]" + \ No newline at end of file diff --git a/Locators/configuration_page.py b/Locators/configuration_page.py new file mode 100644 index 0000000..acf2dc0 --- /dev/null +++ b/Locators/configuration_page.py @@ -0,0 +1,20 @@ +class ConfigurationPageLocators(): + CONFIG_NAVIGATION_PANEL = "//ul/li[3]/div[@class='v-expansion-panel__body']" + + CONFIG_NAVIGATION_PANEL_USER_BUTTON = "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')]" + CONFIG_NAVIGATION_PANEL_NOTIFICATION_BUTTON = "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')][2]" + CONFIG_NAVIGATION_PANEL_MAINTENANCE_BUTTON = "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')][3]" + CONFIG_NAVIGATION_PANEL_ZTP_BUTTON = "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')][4]" + + MAINTENANCE_NAVIGATION_PANEL = \ + "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')][3]/div[@class='v-treeview-node__children']" + MAINTENANCE_NAVIGATION_PANEL_SESSION_BUTTON = \ + "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')][3]/div[@class='v-treeview-node__children']//div[contains(@class,'v-treeview-node--click')]" + MAINTENANCE_NAVIGATION_PANEL_SERVICE_STATUS_BUTTON = \ + "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')][3]/div[@class='v-treeview-node__children']//div[contains(@class,'v-treeview-node--click')][2]" + MAINTENANCE_NAVIGATION_PANEL_LICENSING_BUTTON = \ + "//ul/li[3]/div[@class='v-expansion-panel__body']//div[contains(@class,'v-treeview-node--click')][3]/div[@class='v-treeview-node__children']//div[contains(@class,'v-treeview-node--click')][3]" + + WORK_AREA_TITLE = "//div[@class ='v-toolbar__title']/span" + WORK_AREA_TABLE = "//div[@class='scrollarea__body']/div/div/div/table" + \ No newline at end of file diff --git a/Locators/license_tab.py b/Locators/license_tab.py new file mode 100644 index 0000000..3da8556 --- /dev/null +++ b/Locators/license_tab.py @@ -0,0 +1,5 @@ +class LicenseTabLocators: + JSON_ELEMENT = "//div[@class='jv-node']" + LICENSE_TITLE = "//span[@class='title']" + LICENSE_TITLE_DEVICE_ID = "//span[@class='title text_select']" + LICENSE_INPUT_FORM_TEXTAREA = "//div[contains(@class,'v-input')]/div[@class='v-input__control']//div[@class='v-text-field__slot']/textarea" diff --git a/Locators/login.py b/Locators/login.py new file mode 100644 index 0000000..f5e20b6 --- /dev/null +++ b/Locators/login.py @@ -0,0 +1,4 @@ +class LoginPageLocators: + USERNAME_INPUT = "//input[@type='text']" + PASSWORD_INPUT = "//input[@type='password']" + LOGIN_BTN = "//button[@type='button']" diff --git a/Locators/main_page.py b/Locators/main_page.py new file mode 100644 index 0000000..02cf5bf --- /dev/null +++ b/Locators/main_page.py @@ -0,0 +1,14 @@ +class MainPageLocators: + NAVIGATION_PANEL = "//ul" + NAVIGATION_PANEL_DASHBOARD_BUTTON = "//ul/li[1]/div" + NAVIGATION_PANEL_TOPOLOGY_BUTTON = "//ul/li[2]/div" + NAVIGATION_PANEL_CONFIGURATION_BUTTON = "//ul/li[3]/div" + + NAVIGATION_PANEL_DASHBOARD_BUTTON_HEADER = "//ul/li[1]" + NAVIGATION_PANEL_TOPOLOGY_BUTTON_HEADER = "//ul/li[2]" + NAVIGATION_PANEL_CONFIGURATION_BUTTON_HEADER = "//ul/li[3]" + + + CURRENT_USER_BUTTON = "#app > div.application--wrap > div > div.layout.white > nav > div > div:nth-child(3) > div > div > div > button:nth-child(3)" + CURRENT_USER_WINDOW = "//div[@class='v-card__text']" + \ No newline at end of file diff --git a/Locators/session_locators.py b/Locators/session_locators.py new file mode 100644 index 0000000..322213e --- /dev/null +++ b/Locators/session_locators.py @@ -0,0 +1,31 @@ +class SessionLocators(): + + # Область работы + #TABLE_BODY = "//div[contains(@class, 'scrollarea__body')]//table[@class='scrolltable__container']" + TABLE_BODY = "//div[@class='scrollarea__body']/div/div/div/table" + TEXT_TITLE = "//div[@class ='v-toolbar__title']/span" + + TABLE_SCROLL_CONTAINER = "//div[contains(@class, 'scrollarea__body') and .//table[@class='scrolltable__container']]" # Контейнер со скроллом + TABLE_ROWS = f"{TABLE_SCROLL_CONTAINER}//table/tbody/tr" + #TABLE_SCROLL_CONTAINER = "div.layout.white.column.fill-height" + + # Ячейки таблицы + TABLE_HEADER_CELL_SESSION_ID = "//div[@class='scrollarea__body']//td[1]/div/div" + TABLE_HEADER_CELL_USER_ID = "//div[@class='scrollarea__body']//td[2]/div/div" + TABLE_HEADER_CELL_LIFETIME = "//div[@class='scrollarea__body']//td[3]//div/div" + TABLE_HEADER_CELL_ROLE = "//div[@class='scrollarea__body']//td[4]//div/div" + TABLE_HEADER_CELL_ADDRESS = "//div[@class='scrollarea__body']//td[5]//div/div" + + # Кнопки + BUTTON_DELETE_SESSION = "button.v-btn--icon svg[fill='#4caf50']" # "//div[@class='scrollarea__body']//td[5]//button" + + # Модальное окно + MODAL_WINDOW = "div.v-dialog--active" # Основное модальное окно + MODAL_TITLE = "//*[@id='app']/div[2]/div/div[2]/div[1]" # Заголовк модального окна + #MODAL_SCROLL_CONTAINER_HORIZONTAL = "div.v-dialog--active div.v-card__text" # Горизонтальный скролл + + + MODAL_CLOSE_BUTTON = "//*[@id='app']/div[2]/div/div[1]" # Кнопка закрытия "Х" + MODAL_CANCEL_BUTTON = "div.v-dialog--active button:not(.red--text)" # Кнопка "Отмена" + MODAL_DELETE_BUTTON = "div.v-dialog--active button.red--text" # Кнопка "Удалить" + #SUCCESS_MESSAGE = "селектор сообщения об успешном удалении" # опционально diff --git a/Locators/users_tab.py b/Locators/users_tab.py new file mode 100644 index 0000000..2064b77 --- /dev/null +++ b/Locators/users_tab.py @@ -0,0 +1,14 @@ +class UsersTabLocators: + TOOLBAR_EDIT_BUTTON = "xpath=(.//*[normalize-space(text()) and normalize-space(.)='Пользователи'])[2]/following::*[name()='svg'][1]" + TOOLBAR_ADD_USER_BUTTON = "xpath=(.//*[normalize-space(text()) and normalize-space(.)='Пользователи'])[2]/following::*[name()='svg'][1]" + TOOLBAR_CLOSE_BUTTON = "xpath=(.//*[normalize-space(text()) and normalize-space(.)='Пользователи'])[2]/following::*[name()='svg'][2]" + + USER_DATA_WORK_AREA_TITLE = "//div[@class ='v-toolbar__title']/span[contains(@class,'body-2')]" + ADD_USER_WORK_AREA_CLOSE_BUTTON = "xpath=(.//*[normalize-space(text()) and normalize-space(.)='Добавить нового пользователя'])[1]/following::*[name()='svg'][1]" + + + USER_DATA_INPUT_FORM = "//form[@class='v-form']" + USER_DATA_INPUT_FORM_ROLES_MENU = "//div[contains(@class, 'menuable__content__active')]" + USER_DATA_INPUT_FORM_NOTIFICATION_LABEL = "//label[contains(@class,'v-label')]/span" + + USER_ACTION_CONFIRMATION_DIALOG = "//div[contains(@class, 'v-dialog--active')]//h3" diff --git a/__pycache__/conftest.cpython-313-pytest-8.3.5.pyc b/__pycache__/conftest.cpython-313-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3aa0cd884922435e75cbfa6f2c1c043f50fe13cb GIT binary patch literal 470 zcmX|6ze~eV5PmPQKeZJE2Nwx$UE(B&NKqWb78ESv5)9GCHV~8WURte_mf~m~6hyS* z=vFGCh}M7MI=EyJanh+!{{Szw_Tb*#efQnF?>ezql%Q$fN`3?5r(gV=&`UZ=OwLG# zWN4Z&+Koa=&!K5OJ@2mGhE$^ z^dT?wk416Vr>h$Cf`7}Hhzh)T4|mK$4!7KrjxX%z7@vh1WqoBet-uvDp#k+)Rf1Qj zqd!62N3WqK!3CVdjRN;r*Wgr!OC-O*W2+3!)&V?Xey1$1Ey-{K4IhMO{HctM2aHEZ ztSZ(HSG(dIQ!C*OxxA|ur`6SYWy`jPnH*Af zBy_jNJ_Pm}sGUWPRk|0eMIRJkAA~K68pU>-hd%U?^tc^`aDV{Ym+X^cC)l7*{r^L9 zhBG63m9!5%!2CQs=kVt_-}%pf<2U1(_lN2EziZ&Q-{Xx}LT=^RJ5agDNu0!= z<<9Vw7qK2V8#p6Sfz<_Q57Hp32hWDiglU-7LuV__5K34*e72HSwsAW+siK3E$aHvA zaQM@zxp1`d3%rT)cJM-CPQmMZS~uVtR^HDjNvx5R30-k}>%IE9bTVP2sUAx@%p4kY zZ=S6~57qnM4&ZD ziB!q%s@PqnR1G7lq#B^rQZ3LLsSaqZ6aiW%)dP)4+kn3baLP1KKLJ18tK!XnUMH#?y{IF4}2VozQiK8fse8zo>*I*ue~-Vuz_>EK)6& z47lsV3!;d>XMI49^Y^$*aCORLAkO^&b}`P6@=lA$&2b;|v1q^!>5CbK+C-LBO*LdW zD%e3?NzKw~7-?6^ax#_Bby?PtwZfoWJ2HLo{KVO5^OxrD%-@*n)*aFOS92YH z|7@;f>wWWAqWL%Giut$G=0C#yugt$5Hh%$w|HXV@{n-4Sb;EoB^?#UtBYt|=ylegj z8_nMWryoPdKf(0_7fF2`Upq8ClQ0s~FfA9G;mi`P$LTf} z@EZ0OgmSH8?>9HN$E5aFg+Y>{1Is0&JZ z(TIj9zQ);>Y9lSrC$1^-B27^ga=T_uG33i|FtF)kfo{V_VUen~u%OSmn2G|+BF`YIA=f_(WAXcL?qjR*aAGExOO$h^~ISI!8o{h^tLmGO#8* zB)c5^LWXLjl>c`C(jBmzB8Y~dfk8z3ak(WBRXJebTI^<@v=y40@TskW)SM-QYmE=d zZU)lkr#Ax(zoU2wKJgMf0h-uEyzsnAg73rzW0PNeJ`4gDuq&>qx_UXK&^DMz)DbvG znh+bi7KH7xBD;e?APdrlg65!z=cXWMbj0a$?1*Yx8H{d^h>zlS{Rv4$-MH3h644}k*7S#DpChOE zQm_19p!F>bi^Kz3u2G2wVhkvH6ei?|n!{%a1w=8xy!AP$_bK@;>8cmf3S-JmFzBHX3lODtaj(D#}8} zSc|ek*fVKyAYo5{KkALNd&=M_9fC$kN9E`EEX!Haks|{Z8OV~M zHCXbV@+ISTe5W(!X}1aFP*OZVG$AfX!jbD7Hv`tA72Oecq{4%L7mV2?rRrefl?8eL zdcHtk6F@?*n;rsUhmz?<%>eUJqRY-fXUUOkcVJskv_%;?`m|VI8FHJ9+J!7}esC1) zn|@aLVP&qq->UCly|QkY_5Ei3(OmsWtNvuR{`H%ojY!i9e|y;^FRkp)l9z5Rm!C)U zl^L?A81HdY4M(Q(I0$|m9G48W3@*|RCz8fuB4raN)>E9rQ?l3^S4Q50k-FVlwmyY* z>;-<$(A$M2MVhk_uyO;dFy8?y(zBHb7dEbZ!t?_0rQZWmGPQbVYFgTmomtqOWz#Ek zqLrrKN74+W$kD^#fdM!J6@MhRSs1c|o9bcVgRr@G3}T4z*Dx^@>NqJF4?@UMw616< zW>@M<3k!-t)g*@7`jTcO-lw>g%T>eT>!(kh5>;J<@Q_^5)7p$WJ8K6S6;g`kX+HGm zsf2{tT}&91I)vLCkdTRoQdb#DzFw3U*Q0qHX}$BqpZsuTYW=DeIe9bqIMQ%;^e?QPc+kWrgWQyCI!kh@G%yA}hMgaJwxf(gYFoL-nbQ)=ytt+o)BLgPcwFZK=%Zdz???T>_ zdIYMB?E+j&%cZ=KO`sMoDS)=yFM^;-0W?{O9W3(({!?l3@gk*tnEH_fFR3hY=l>G; z1-HzPrNN7N0>6-t-$mdT-ZDQhyQKyARrvVr1%Bjz$*b}RO%b62i&%<3HbWOAeR z#Mc5}JH~%r+q!Z%TifR!GH!K_e`0L3Ucey}Y{-FZZO>*yUc(_LU&v(&hrG={33Ii_ z%#Neknxl6|*@OozGWcldP;O}48XEs(abrk^k(CL)TtVi|)PKWrznhub%tU#I98~6( zbLj<-aI+5+p0A4G>jawklH$E_0>2TI6t6GNl(*wMfLeD)v9LD~TW<9oIU}7;8EQsk zS@#Mj_C;H0hvw7QKxZ()ub4c4{@mp03y`FTv|Qey2RUrXu@V zAudNSNCa~rIvG{WU!WNI5{MDV3ZYk+P&JllE!skiV>U~G_c_}K&*di}JCks?l6_y; zTeg=keqbKnrenYW8~dWqGrbmps3A*sn!}T3{B3jcl6mQhx%=uva+RHRCkp7qkErOW z$+BG~%M0n5#T3?SWEoQuZcjv(VF|jCQZ+?O%QD3cW?wJqOHi`~kZY#i=}zom1_0eP zdK?MrXo~JKeGADX67ZBb`Yw_MB<~?XfuSED`70zp1!6bDM_dE;FIjLv;k&L~>we-* zDybBF{^e-{4A#+sb7B^XAD$V(ivANI-+aJ53GkurmB}X@TvliPh?l2fKJ@3HCIg|} ztNK?QT%M3XXwOMXw^C|vr+t(2PLxsuF{{WOlW1|27 literal 0 HcmV?d00001 diff --git a/data/__pycache__/constants.cpython-313.pyc b/data/__pycache__/constants.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c65347577154840f849216ebf550bc99fca0347 GIT binary patch literal 790 zcmZ`%Pe>F|82{eP&N{ocDsDz}&?y30sNKB?86_-QCL6oqC|(90ll#Yk-I?{xsMsky zh}6N7#3jn;*eNk0i84BNdy9u`I(D*?v_a9SZ)RNS(u4PV-}n1|@B2Q!_nNU-6mZRc z*JeK9`OR}$D|BQu6*SgC1(jrh!D%=Ls&EEWaX<(ZjFd?tbl^F;>3|CGIKw^k*FghD zRA7QinAi(iXxy*;?*;I450HRk;s0vHV@;bXv1mDb7BsmH^ca8wS_C@`wG;~UL7*wY zRk06BB+=$ZhAq=}bkng9L@ee;Gm{fH=*|4-Sk8_4_N|PnPUXi(?)3Vsk{?%Elroi3 zbNv&__55UR#&A4LGz`(H9q(QJ!(={3WfR zJGwUAnr*VS$h<$`gEwb;2w@oxg^-lq@9N%&tVQY<_PY94r2Tl$#^Bmuo$kdi-j{#x zx5t}6r1Td##ygUq`qLvay|twJ2Ha!kq|^be?nmR`avy!D(s}^_8_?O fmEcW=*eP_q?`3x*U`9f|LgZISBIzH1;NSTR{ZZs1 literal 0 HcmV?d00001 diff --git a/data/__pycache__/environment.cpython-313.pyc b/data/__pycache__/environment.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a54056b42195ca8effec7107967a1a92826b288a GIT binary patch literal 1760 zcmZ`(-A^1<6hHH^-wO*YWXnQfK&)L z$%;uslPaxBphjDL@PRi{V??8E{0}Z6n$6fZeW_0f8jVk$JImsh(39-%p6@;PoO5Tt zrp6CQSAJJ!e&zvwWkQ9-17<&t%qqwrbHhO31ayMTM?n^PFoF@`6s{9`$^sBR!^Ol$ zOiHB>bk1>@?^P66LBTi~y=9IFGEYQVATC9eMa3n%PV>YqyQ4rnvIo&Cdl7xI5796C zNgxx51?@o6SjdvRF{>NaBLQ>TqNSUb?U~XS^jv;!v2n(-=FW9@pXomvKhxV6KO29s zw|lYah_>f+ytltQ%{euefmO;9?%zOIg*KQd>k&r|?tG{~2K&MF9K?@7?$AT#QDS)< z%0LH9cq@_zv_=34#$d#mKt*3ZtFyF22BzU~@uiNEjYCez-fcO?1=U8i3t^lOyw(2wKvQ*{1;Zf!5o``bmjh5T2_;Mn;% zy-DveCH)>VEurBXJg=eSiek)SQ8*fPE~gc)l5A$iQWyG^Db3OpEW9$r_*}vEs%qBA zTB>R{R*g4a(FU<>vmc=dyS(V@co=D3D|}e^ylE%$%yQz!P)X`sPW%)+`Om{pWFI(R zM=8>>R=8DIPJA%5E1>+Z8N%qL&Zptame&N~N1j7Ge)#L?xXK2E=TSoK;h6uoy@agR z3@qjt>+zXf7}Et4>~Kitj`ypD7x>DO>0FFYEw+srmyP^YLt4;s^SYFumT)srAX#lP zr%N$`)L=$i92>bH+rH%6X?@Ph<_%Jh+VU2VFmnjv4yluxsjKrOXP>BUd$oo6nga+$ zC0QN$hlA_2wX#=`|oJ2EeV{o*kb*ynuF6pSX6?2$u zwXvj7cxCbWBK0;qb>R?e*&nb;IyX<@hY#O~q0Q>}M*ONeo1dD`F+QlOSLU_cfhDA> z(^+C#xvZfZc~vE>y|yQjyp+6f@l6t8`S1e-gtdc644y);n^e`bG%K4{HOnH|$$5OY zs=5JAHBP^Y$ebCl9+|xe*Ki~1i-}Tgq&T?i244-W?J9XgwEo#$mk@Lp6MKG_t7ETL zbe-q+f}E@6FE{6kmQ%7sc*_7DPEuQ($(t5=j>&8? None: + super().__init__(page) + + def check_URL(self, uri, msg): + expect(self.page).to_have_url(f"{host.get_base_url()}{uri}", timeout=60000), msg + + def have_text(self, locator, text: str, msg): #элемент имеет текст + loc = self.page.locator(locator) + expect(loc).to_have_text(text), msg + + def have_title(self, locator, title: str, msg): + loc = self.page.locator(locator) + expect(loc).to_have_text(title), msg + + def check_presence(self, locator, msg): + loc = self.page.locator(locator) + expect(loc).to_be_visible(visible=True, timeout=12000), msg + + def check_button_presence_with_text(self, text, msg): + loc = self.page.get_by_role("button", name=text) + expect(loc).to_be_visible(visible=True, timeout=12000), msg + + def check_absence(self, locator, msg): + loc = self.page.locator(locator) + expect(loc).to_be_hidden(timeout=700), msg + + def check_absence_after_period(self, locator, period, msg): + loc = self.page.locator(locator) + expect(loc).to_be_hidden(timeout=period), msg + + def check_empty_input_area(self, input_area, msg): + expect(input_area).to_be_empty(), msg + + def check_menu_item_with_text(self, text, msg): + count = self.page.get_by_role("listitem", name=text).count() + assert count != 1, msg + + def check_equals(self, actual, expected, msg): + assert actual == expected, msg + + def check_not_equals(self, actual, expected, msg): + assert actual != expected, msg + + def check_json_equals(self, actual, expected, msg): + diff = jsondiff.diff(expected, actual, syntax='symmetric') + assert len(diff) == 0, f"{msg}. DIFF is {diff}" + + def check_lists_equals(self, actual, expected, msg): + def compare_lists(list1, list2): + if len(list1) != len(list2): + return False + for item1, item2 in zip(list1, list2): + if isinstance(item1, list) and isinstance(item2, list): + if not compare_lists(item1, item2): + return False + elif item1 != item2: + return False + return True + + assert compare_lists(actual, expected), msg + + def check_url_content(self, uri,msg): + assert f"{uri}" in self.page.url, msg + + def check_element_active(self, locator, msg): + element_handle = self.page.wait_for_selector(locator) + class_names = element_handle.get_attribute('class') + assert 'active' in class_names, msg + + def check_alert_window_with_text(self, alert_type, text): + self.check_presence(BasePageLocators.ALERT_WINDOW, "No alert window for action notification") + + if alert_type == "error": + self.have_text(BasePageLocators.ALERT_WINDOW_TEXT_ERROR, text, \ + "Unexpected error message in alert window") + elif alert_type == "success": + self.have_text(BasePageLocators.ALERT_WINDOW_TEXT_SUCCESS, text, \ + "Unexpected message about success action in alert window") + elif alert_type == "info": + self.have_text(BasePageLocators.ALERT_WINDOW_TEXT_WARNING, text, \ + "Unexpected info message in alert window") + elif alert_type == "warning": + self.have_text(BasePageLocators.ALERT_WINDOW_TEXT_WARNING, text, \ + "Unexpected warning message about success action in alert window") + else: + assert False, "Unsupported type of alert window" + self.check_absence_after_period(BasePageLocators.ALERT_WINDOW, 30000, \ + "Alert window for action notification should disappear") + + def check_tooltip_with_text(self, locator, text): + self.page.locator(locator).hover() + tooltip = self.page.locator(BasePageLocators.TOOLTIP) + assert tooltip.text_content().strip() == text, "Unexpected tooltip text" + + def check_confirmation_dialog_with_title(self, locator, title): + loc = self.page.locator(locator) + expect(loc).to_contain_text(title), f"Confirmation dialog window with title {title} is not present" + \ No newline at end of file diff --git a/data/constants.py b/data/constants.py new file mode 100644 index 0000000..e733910 --- /dev/null +++ b/data/constants.py @@ -0,0 +1,9 @@ +import os + +class Constants: + try: + login = os.getenv('AUTH_LOGIN') + password = os.getenv('AUTH_PASSWORD') + except KeyError: + print("LOGIN OR PASSWORD WASN'T FOUND") + \ No newline at end of file diff --git a/data/environment.py b/data/environment.py new file mode 100644 index 0000000..4c8e089 --- /dev/null +++ b/data/environment.py @@ -0,0 +1,31 @@ +import os + +class Environment: + TEST = 'test' + DEVELOP = 'develop' + + URLS = { + TEST: 'http://192.168.2.76/', + DEVELOP: 'http://192.168.50.69/' + } + + def __init__(self): + try: + self.env = os.getenv('ENV') + self.access_token = "" + except KeyError: + self.env = self.TEST + + def get_base_url(self): + if self.env in self.URLS: + return self.URLS[self.env] + else: + raise Exception(f"Unknown value of ENV variable {self.env}") + + def set_access_token(self, token): + self.token = token + + def get_access_token(self): + return self.token + +host = Environment() diff --git a/fixtures/__pycache__/pages.cpython-313-pytest-8.3.5.pyc b/fixtures/__pycache__/pages.cpython-313-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22668b26414bcc30e14a298fd899774f37cca4c7 GIT binary patch literal 4994 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 literal 0 HcmV?d00001 diff --git a/fixtures/pages.py b/fixtures/pages.py new file mode 100644 index 0000000..dca3e66 --- /dev/null +++ b/fixtures/pages.py @@ -0,0 +1,95 @@ +import pytest +from playwright.sync_api import Browser, BrowserContext, Page, sync_playwright +import os + + + +def pytest_addoption(parser): + """Пользовательские опции командной строки""" + parser.addoption('--bn', action='store', default="chrome", help="Choose browser: chrome, remote_chrome or firefox") + parser.addoption('--h', action='store', default=False, help='Choose headless: True or False') + parser.addoption('--s', action='store', default={'width': 1520, 'height': 380}, help='Size window: width,height') + # parser.addoption('--s', action='store', default={'width': 1920, 'height': 300}, help='Size window: width,height') + parser.addoption('--slow', action='store', default=200, help='Choose slow_mo for robot action') + parser.addoption('--t', action='store', default=60000, help='Choose timeout') + parser.addoption('--l', action='store', default='ru-RU', help='Choose locale') + # parser.addini('qs_to_api_token', default=os.getenv("QASE_TOKEN"), help='Qase app token') + + + +@pytest.fixture(scope='class') +def browser(request) -> Page: + playwright = sync_playwright().start() + if request.config.getoption("bn") == 'remote_chrome': + browser = get_remote_chrome(playwright, request) + context = get_context(browser, request, 'remote') + page_data = context.new_page() + elif request.config.getoption("bn") == 'firefox': + browser = get_firefox_browser(playwright, request) + context = get_context(browser, request, 'local') + page_data = context.new_page() + elif request.config.getoption("bn") == 'chrome': + browser = get_chrome_browser(playwright, request) + context = get_context(browser, request, 'local') + page_data = context.new_page() + else: + browser = get_chrome_browser(playwright, request) + context = get_context(browser, request, 'local') + page_data = context.new_page() + yield page_data + for context in browser.contexts: + context.close() + browser.close() + playwright.stop() + + +def get_firefox_browser(playwright, request) -> Browser: + return playwright.firefox.launch( + headless=request.config.getoption("h"), + slow_mo=request.config.getoption("slow"), + ) + + +def get_chrome_browser(playwright, request) -> Browser: + return playwright.chromium.launch( + headless=request.config.getoption("h"), + slow_mo=request.config.getoption("slow"), + args=['--s'] + ) + +def get_remote_chrome(playwright, request) -> Browser: + return playwright.chromium.launch( + headless=True, + slow_mo=request.config.getoption("slow") + ) + + +def get_context(browser, request, start) -> BrowserContext: + if start == 'local': + context = browser.new_context( + # no_viewport=True, + viewport=request.config.getoption('s'), + locale=request.config.getoption('l') + ) + context.set_default_timeout( + timeout=request.config.getoption('t') + ) + # context.add_cookies([{'url': 'https://example.ru', 'name': 'ab_test', 'value': 'd'}]) добавляем куки, если нужны + return context + + elif start == 'remote': + context = browser.new_context( + viewport=request.config.getoption('s'), + locale=request.config.getoption('l') + ) + context.set_default_timeout( + timeout=request.config.getoption('t') + ) + # context.add_cookies([{'url': 'https://example.ru', 'name': 'ab_test', 'value': 'd'}]) добавляем куки, если нужны + return context + + + +@pytest.fixture(scope="function") +def return_back(browser): + browser.go_back() \ No newline at end of file diff --git a/fixtures/pages.py.or b/fixtures/pages.py.or new file mode 100644 index 0000000..8361343 --- /dev/null +++ b/fixtures/pages.py.or @@ -0,0 +1,93 @@ +import pytest +from playwright.sync_api import Browser, BrowserContext, Page, sync_playwright +import os + + + +def pytest_addoption(parser): + """Пользовательские опции командной строки""" + parser.addoption('--bn', action='store', default="chrome", help="Choose browser: chrome, remote_chrome or firefox") + parser.addoption('--h', action='store', default=False, help='Choose headless: True or False') + parser.addoption('--s', action='store', default={'width': 1920, 'height': 1080}, help='Size window: width,height') + parser.addoption('--slow', action='store', default=200, help='Choose slow_mo for robot action') + parser.addoption('--t', action='store', default=60000, help='Choose timeout') + parser.addoption('--l', action='store', default='ru-RU', help='Choose locale') + # parser.addini('qs_to_api_token', default=os.getenv("QASE_TOKEN"), help='Qase app token') + + + +@pytest.fixture(scope='class') +def browser(request) -> Page: + playwright = sync_playwright().start() + if request.config.getoption("bn") == 'remote_chrome': + browser = get_remote_chrome(playwright, request) + context = get_context(browser, request, 'remote') + page_data = context.new_page() + elif request.config.getoption("bn") == 'firefox': + browser = get_firefox_browser(playwright, request) + context = get_context(browser, request, 'local') + page_data = context.new_page() + elif request.config.getoption("bn") == 'chrome': + browser = get_chrome_browser(playwright, request) + context = get_context(browser, request, 'local') + page_data = context.new_page() + else: + browser = get_chrome_browser(playwright, request) + context = get_context(browser, request, 'local') + page_data = context.new_page() + yield page_data + for context in browser.contexts: + context.close() + browser.close() + playwright.stop() + + +def get_firefox_browser(playwright, request) -> Browser: + return playwright.firefox.launch( + headless=request.config.getoption("h"), + slow_mo=request.config.getoption("slow"), + ) + + +def get_chrome_browser(playwright, request) -> Browser: + return playwright.chromium.launch( + headless=request.config.getoption("h"), + slow_mo=request.config.getoption("slow"), + args=['--start-maximized'] + ) + +def get_remote_chrome(playwright, request) -> Browser: + return playwright.chromium.launch( + headless=True, + slow_mo=request.config.getoption("slow") + ) + + +def get_context(browser, request, start) -> BrowserContext: + if start == 'local': + context = browser.new_context( + no_viewport=True, + locale=request.config.getoption('l') + ) + context.set_default_timeout( + timeout=request.config.getoption('t') + ) + # context.add_cookies([{'url': 'https://example.ru', 'name': 'ab_test', 'value': 'd'}]) добавляем куки, если нужны + return context + + elif start == 'remote': + context = browser.new_context( + viewport=request.config.getoption('s'), + locale=request.config.getoption('l') + ) + context.set_default_timeout( + timeout=request.config.getoption('t') + ) + # context.add_cookies([{'url': 'https://example.ru', 'name': 'ab_test', 'value': 'd'}]) добавляем куки, если нужны + return context + + + +@pytest.fixture(scope="function") +def return_back(browser): + browser.go_back() \ No newline at end of file diff --git a/pages/__pycache__/base_page.cpython-313.pyc b/pages/__pycache__/base_page.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..360b8c762df1bed02d1c438a956cf516c631b79b GIT binary patch literal 16110 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 literal 0 HcmV?d00001 diff --git a/pages/__pycache__/login_page.cpython-313.pyc b/pages/__pycache__/login_page.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99b3449316b69d29880547edfa776f6219c94b2b GIT binary patch literal 4120 zcmb7HU2GKB6`t9hS&eurSx&87}**@RjE(uOC32VdF(lNcJ{~J z(lYj*d+xdC{@?R`_pZm|F$TWLe`cos0NOt=6MTF{S)TyqDkC#8JIai(gcVT_jfO_T zBuw@2C^y0rp6c9aWTb_(P@NwYMxrE2^~h*!q?NRaOc&F~$SpmLEM(X{VZRU3HW`&- zUtvaKJ>f;oGLCDL2I$e33Z`XirforH^U=a&&cu?V1zocX1dXv_%QA?a13$=dQw7V$ z6uor6bzfGe^BUxT0TaqFgq0Z*l35a#Lm5sE%iL~GVRo}J-@}{|PH}{nTjfY5BDZ8B zyE$0^D|d<|Epl6(HCnL>a*RY3WMrawO@@IXBSg+=iAeK*3E&-vr0mj5%$owqe<7F|VCFLvoW-w)*<>YI-<> z>uF{3cqCNRlf`pnGYdGgBsYbzWZ3Xe&dFGxFzue*qUDrC7g&(YLdEyc`6T8>F zb^hf$-O?rQt@d?rV?3b*r^?i(4NoY-^v@JXR>uVhK3reAXFr0Sz~e*@4vPoJ439Cm zlN-Nuef>(I98$u2Dtjq1P%k*9b(Q6CZSNI!0=yOWSYjNvjeco7UmHG?Q$o};)Zj^@ zz0uX=q1Sk$4Rhn|fi1l$&}wOIL{PhJ zTw#JmGg*b*4!-zlpeSt9BOj{zhaP^=`xvF_D>9SxPYS0zo&tl{rs=u?=tYqZFfd#q zd2jQiVXMTjioh9$I#I};^TJ>vh@{>o3kn-*ah?!3d+|V{)yQK|gWwUf5D!E^NAfr} zl9~7gr?giEw2V~P&EYc zU?Dud20J2({+0;Ay4UkVU_-34z61eF{vg6fcdfNE(fDlia&)%sa@*X`=AT>^cKuyw zL(5#>OnfcEJTvI_?|XOqpQB$U{*<_sSnW?%@+$|^?!ok>1h}sVT|hL;-8)vg_qyGC zmxX<+&_?WXY`J6GO2?4fF;rHUg+q_zS*@OQJCe)7Adag2a{G!Px`H_WJtvvIE68-e zoAcFs5Z)NZW*7pwKGb3M_{nwQ1RhN|YrJ~5NLM+?uZuDWToD*Cjl8?zdw6ZQ!&KSu zfA-#l%^somCba=d>U(d-OSQRI`LQ;^hTfa7W;3Rg!}T`+ULTm=8Xam>4B#@?VgVQY-9}Z~I{cUL%&iQ>9 zKxGb4-@j7AsLCRAvgCY%H^xC2X&G!-dxnCU#~XwcNJ@$k2^t|gP#^}8XHg8G*oh*E zVh{zzm|h=av^=~X)H69VTQ~!WJq6dzv$n)-fE}+VXX*t)03=nN7+J%#a{%7dIRO6( z(rf`#DVbJ@GSA6U-iLed$p#AePX`>g^)`s}48TkO3Dyz%>LBO;1Bq9cg#(Y|`Cs8= z$Q6dlhn@Ye+!bD>a3Y21x>3%9Cq+sMd4g4#HV}d-3rMZ_w!k|xye(p?I$g+?fCE*p zO;vvhU|lImsOrfavFv=#G|Ymk61=|pH}|k*6EcL^y&$|T@aV8LJEyCfZIj$Y2_6E_ zT)grm(T#jwRV9{e0cQ)L>>!1Df{{%Hwru?b#CLBpYoRc|o`Wl1jU$%Zr({(C+9>w-k@We~dCrEMvo~!f)LJ%yy_N-=W zNj-SCQmXXYgBNR(a=*`!3F=vWcv=2SY!kH_=Ic39FsI?1`S+5J+5;$QoikxgiaCGj za5x_`{?O>{LwLbo`U7MHif~~pB+a!j%d+>GZS;TOKGT1nc?$GzxLxcCc20P}U`m_$ EH+Nen+a literal 0 HcmV?d00001 diff --git a/pages/__pycache__/main_page.cpython-313.pyc b/pages/__pycache__/main_page.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..890174296e05656a569f5e356c8e48c3010d699f GIT binary patch literal 3288 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 HAhiAu8+Bz8 literal 0 HcmV?d00001 diff --git a/pages/base_page.py b/pages/base_page.py new file mode 100644 index 0000000..0cf5c12 --- /dev/null +++ b/pages/base_page.py @@ -0,0 +1,296 @@ +from playwright.sync_api import Page, TimeoutError, Response, APIRequestContext, expect +from data.environment import host +from Locators.base_page import BasePageLocators +import time +import json + +class BasePage: + def __init__(self, page: Page): + self.page = page + self.token = "" + + def open(self, uri) -> Response | None: + return self.page.goto(f"{host.get_base_url()}{uri}", wait_until='domcontentloaded') + + def get_api_request_context(self) -> APIRequestContext: + return self.page.context.request + + #return page url + def current_url(self) -> str: + return self.page.url + + # click on element + def click(self, locator: str) -> None: + self.page.click(locator) + + # input in field + def input(self, locator: str, data: str) -> None: + self.page.locator(locator).fill(data) + + # clear input field + def clear_input(self, locator: str) -> None: + self.page.locator(locator).press('Control+A') + self.page.locator(locator).press('Backspace') + + def page_reload(self) ->None: + self.page.reload() + + # wait for element + def wait_for_element(self, locator, timeout=12000) -> None: + self.page.wait_for_selector(locator, timeout=timeout) + + # wait for all elements + def wait_for_all_elements(self, locator, timeout=5000): + elements = self.page.query_selector_all(locator) + + for element in elements: + self.page.wait_for_selector(locator, timeout=timeout) + + return elements + + # if element presents then ok + def is_element_present(self, locator: str, timeout: int = 5000) -> bool: + try: + self.page.wait_for_selector(locator, timeout=timeout) + except TimeoutError: + return False + return True + + # if element is not present then ok + def is_element_NOT_presence(self, locator: str, timeout: int = 5000) -> bool: + try: + self.page.wait_for_selector(locator, timeout=timeout) + except TimeoutError: + return True + return False + + # get text. If we have once locator then index is 0 + def get_text(self, locator: str, index: int) -> str: + return self.page.locator(locator).nth(index).text_content() + + # Traverse the table + def read_table_data(self, locator) -> []: + table_data = [] + + table = self.page.locator(locator) + + # read table header + header_cells = table.locator(BasePageLocators.TABLE_HEADERS_CELLS) + header_data = [] + for h in range(header_cells.count()): + header_cell_text = header_cells.nth(h).inner_text() + header_data.append(header_cell_text) + table_data.append(header_data) + + # read table cells by rows + rows = table.locator(BasePageLocators.TABLE_BODY) + for i in range(rows.count()): + row = rows.nth(i) + cells = row.locator("td") + row_data = [] + for j in range(cells.count()): + cell_text = cells.nth(j).inner_text() + row_data.append(cell_text) + table_data.append(row_data) + + return table_data + + # send get request to api + def send_get_api_request(self, uri): + api_request_context = self.get_api_request_context() + token = host.get_access_token() + headers = {"Accept": "application/json", "Authorization": f"Bearer {token}"} + response = api_request_context.get(f"{host.get_base_url()}{uri}", headers = headers) + return response + + # send get request to api + def send_post_api_request(self, uri, payload): + api_request_context = self.get_api_request_context() + token = host.get_access_token() + headers = {"Accept": "application/json", "Authorization": f"Bearer {token}"} + response = api_request_context.post(f"{host.get_base_url()}{uri}", headers = headers, data=payload) + return response + + # get response body + def get_response_body(self, response): + try: + response_body = response.json() + except json.JSONDecodeError: + print("Failed to decode JSON response") + return None + return response_body + + def should_be_horizontal_scroll(self) -> None: + """ + Проверяет горизонтальный скролл с плавной прокруткой. + """ + modal_selector = "div.v-dialog.v-dialog--active" + modal = self.page.locator(modal_selector).first + + try: + # Ожидаем появления окна + modal.wait_for(state="visible", timeout=5000) + + # Получаем размеры через page.evaluate + scroll_data = self.page.evaluate("""() => { + const modal = document.querySelector('div.v-dialog.v-dialog--active'); + return { + scrollWidth: modal.scrollWidth, + clientWidth: modal.clientWidth, + scrollLeft: modal.scrollLeft + }; + }""") + + print(f"Окно: Общая ширина: {scroll_data['scrollWidth']}px, " + f"Видимая область: {scroll_data['clientWidth']}px, " + f"Начальная позиция: {scroll_data['scrollLeft']}px") + + if scroll_data['scrollWidth'] > scroll_data['clientWidth']: + print("Тестируем скролл...") + + # Плавный скролл вправо + self.page.evaluate("""() => { + const modal = document.querySelector('div.v-dialog.v-dialog--active'); + return new Promise(resolve => { + const target = modal.scrollWidth - modal.clientWidth; + const duration = 1000; + const start = modal.scrollLeft; + const startTime = performance.now(); + + function scrollStep(timestamp) { + const progress = (timestamp - startTime) / duration; + modal.scrollLeft = start + (target - start) * Math.min(progress, 1); + if (progress < 1) { + window.requestAnimationFrame(scrollStep); + } else { + resolve(); + } + } + window.requestAnimationFrame(scrollStep); + }); + }""") + + # Проверяем конечную позицию + final_scroll = self.page.evaluate("""() => { + return document.querySelector('div.v-dialog.v-dialog--active').scrollLeft; + }""") + print("Успешно проскроллили вправо") + + # Плавный скролл влево + self.page.evaluate("""() => { + const modal = document.querySelector('div.v-dialog.v-dialog--active'); + return new Promise(resolve => { + const duration = 1000; + const start = modal.scrollLeft; + const startTime = performance.now(); + + function scrollStep(timestamp) { + const progress = (timestamp - startTime) / duration; + modal.scrollLeft = start - start * Math.min(progress, 1); + if (progress < 1) { + window.requestAnimationFrame(scrollStep); + } else { + resolve(); + } + } + window.requestAnimationFrame(scrollStep); + }); + }""") + + # Проверяем возврат в начало + final_scroll = self.page.evaluate("""() => { + return document.querySelector('div.v-dialog.v-dialog--active').scrollLeft; + }""") + assert final_scroll == 0, "Не удалось вернуться в начальную позицию скролла" + print("Успешно проскроллили влево") + else: + print("Окно не требует горизонтального скролла.") + + except Exception as e: + print(f"Ошибка при проверке скролла окна: {str(e)}") + raise + + def should_be_vertical_scroll(self, wrapper_selector: str = "div.scrolltable__wrapper") -> None: + """ + Проверяет вертикальный скролл в указанном контейнере. + Args: + wrapper_selector: CSS-селектор скроллируемого контейнера. + По умолчанию: "div.scrolltable__wrapper". + """ + # Ожидаем загрузки таблицы + table_wrapper = self.page.locator(wrapper_selector).first + table_wrapper.wait_for(state="visible", timeout=10000) + + try: + # Пытаемся найти скроллируемый элемент с небольшим таймаутом + scrollable_element = table_wrapper.locator("tbody.scrolltable__scroll").first + scrollable_element.wait_for(state="attached", timeout=3000) + + # Проверяем параметры скролла + scroll_height = scrollable_element.evaluate("el => el.scrollHeight") + client_height = scrollable_element.evaluate("el => el.clientHeight") + scroll_data = scrollable_element.evaluate("el => el.scrollLeft") + + + print(f"Окно: Общая ширина: {scroll_height}px, " + f"Видимая область: {client_height}px, " + f"Начальная позиция: {scroll_data}px") + + if scroll_height > client_height: + print("Тестируем скролл...") + + # 1. Плавный скролл вниз + scrollable_element.evaluate("""el => { + const target = el.scrollHeight; + const duration = 1000; + const start = el.scrollTop; + const startTime = performance.now(); + + function scrollStep(timestamp) { + const progress = (timestamp - startTime) / duration; + el.scrollTop = start + (target - start) * Math.min(progress, 1); + if (progress < 1) { + window.requestAnimationFrame(scrollStep); + } + } + window.requestAnimationFrame(scrollStep); + }""") + time.sleep(1.5) # Ожидаем завершения скролла + + # Проверяем последнюю строку + last_row = table_wrapper.locator("tbody tr").last + expect(last_row).to_be_visible() + print("Успешно проскроллили вниз") + + # 2. Плавный скролл вверх + scrollable_element.evaluate("""el => { + const duration = 1000; + const start = el.scrollTop; + const startTime = performance.now(); + + function scrollStep(timestamp) { + const progress = (timestamp - startTime) / duration; + el.scrollTop = start - start * Math.min(progress, 1); + if (progress < 1) { + window.requestAnimationFrame(scrollStep); + } + } + window.requestAnimationFrame(scrollStep); + }""") + time.sleep(1.5) # Ожидаем завершения скролла + + # Проверяем первую строку + first_row = table_wrapper.locator("tbody tr").first + expect(first_row).to_be_visible() + print("Успешно проскроллили вверх") + else: + print("Весь контент виден без скролла") + + except Exception as e: + print(f"Элемент для скролла не найден или не скроллится: {e}") + # Проверяем, есть ли хотя бы строки в таблице + rows = table_wrapper.locator("tbody tr") + if rows.count() > 0: + print(f"Найдено {rows.count()} строк в таблице без скролла") + else: + print("Таблица пуста или не найдена") \ No newline at end of file diff --git a/pages/configuration_page.py b/pages/configuration_page.py new file mode 100644 index 0000000..d12b6f8 --- /dev/null +++ b/pages/configuration_page.py @@ -0,0 +1,49 @@ +from pages.base_page import BasePage +from Locators.configuration_page import ConfigurationPageLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +import json + +class ConfigurationPage(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) + + def should_be_configuration_navigation_panel(self): + self.assertion.check_presence(ConfigurationPageLocators.CONFIG_NAVIGATION_PANEL, \ + "Configuration navigation panel is not present on the Configuration page") + def should_be_maintenance_navigation_panel(self): + self.assertion.check_presence(ConfigurationPageLocators.MAINTENANCE_NAVIGATION_PANEL, \ + "Maintenance navigation panel is not present on the Configuration page") + + def click_configuration_navigation_panel_item(self, item_name): + button_locator = None + + if item_name == "users": + button_locator = ConfigurationPageLocators.CONFIG_NAVIGATION_PANEL_USER_BUTTON + elif item_name == "notification": + button_locator = ConfigurationPageLocators.CONFIG_NAVIGATION_PANEL_NOTIFICATION_BUTTON + elif item_name == "maintenance": + button_locator = ConfigurationPageLocators.CONFIG_NAVIGATION_PANEL_MAINTENANCE_BUTTON + elif item_name == "ztp": + button_locator = ConfigurationPageLocators.CONFIG_NAVIGATION_PANEL_ZTP_BUTTON + else: + assert False, "Unsupported configuration navigation panel item" + + self.click(button_locator) + + def click_maintenance_navigation_panel_item(self, item_name): + button_locator = None + + if item_name == "session": + button_locator = ConfigurationPageLocators.MAINTENANCE_NAVIGATION_PANEL_SESSION_BUTTON + elif item_name == "service_status": + button_locator = ConfigurationPageLocators.MAINTENANCE_NAVIGATION_PANEL_SERVICE_STATUS_BUTTON + elif item_name == "licensing": + button_locator = ConfigurationPageLocators.MAINTENANCE_NAVIGATION_PANEL_LICENSING_BUTTON + else: + assert False, "Unsupported configuration navigation panel item" + + self.click(button_locator) + \ No newline at end of file diff --git a/pages/email_tab.py b/pages/email_tab.py new file mode 100644 index 0000000..23c4fe9 --- /dev/null +++ b/pages/email_tab.py @@ -0,0 +1,9 @@ +from pages.base_page import BasePage +# from Locators.email_tab import EmailTabLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +class EmailTab(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) diff --git a/pages/license_tab.py b/pages/license_tab.py new file mode 100644 index 0000000..67c48b1 --- /dev/null +++ b/pages/license_tab.py @@ -0,0 +1,102 @@ +from pages.base_page import BasePage +from Locators.configuration_page import ConfigurationPageLocators +from Locators.license_tab import LicenseTabLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +import json + +class LicenseTab(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) + + def fill_license_input_form(self, value): + button_text = "Обновить лицензию" + + self.clear_input(LicenseTabLocators.LICENSE_INPUT_FORM_TEXTAREA) + self.input(LicenseTabLocators.LICENSE_INPUT_FORM_TEXTAREA, value) + + self.page.get_by_role("button", name=button_text).click() + + def should_be_error_alert_window_with_text(self, text): + self.assertion.check_alert_window_with_text("error", text) + + def should_be_license_work_area(self): + self.assertion.have_title(ConfigurationPageLocators.WORK_AREA_TITLE, "Лицензирование", \ + "Expected work area page title is not equal real title") + self.should_be_json_content() + self.should_be_license_input_form() + + def should_be_license_input_form(self): + # get form title and compare with cmdb value + form_title = "Идентификатор:" + title = self.get_text(LicenseTabLocators.LICENSE_TITLE, 0) + self.assertion.check_equals(title.strip(), form_title, \ + f"Expected input form title {title} is not equal {form_title}") + + device_id = self.get_text(LicenseTabLocators.LICENSE_TITLE_DEVICE_ID, 0).strip() + + response = self.send_get_api_request("e-cmdb/api/lic/deviceid") + response_body = self.get_response_body(response) + + self.assertion.check_equals(device_id, response_body["deviceId"], \ + f"Expected ID value {device_id} is not equal {response_body['deviceId']}") + + # check input form presence + self.assertion.check_presence(LicenseTabLocators.LICENSE_INPUT_FORM_TEXTAREA, \ + "License input form is not present on the License work area") + # check input form button + button_text = "Обновить лицензию" + self.assertion.check_button_presence_with_text(button_text, f"License input form button with text {button_text} is not present") + + def should_be_json_content(self): + def format_json_string(json_string): + substrings = json_string.splitlines() + + formatted_string_list = [] + + last_substring = substrings.pop() + + for substring in substrings: + if substring.find(':') == -1: + if substring == '{': + formatted_string_list.append(substring) + elif substring == '}': + s1 = formatted_string_list.pop() + formatted_string_list.append(s1.rstrip(',')) + formatted_string_list.append(substring+ ',') + else: + formatted_string_list.append(substring + ',') + continue + + key, value = substring.split(':') + + s = ':'.join(['"'+key+'" '," " + value]) + if value == '{': + formatted_string_list.append(s) + else: + formatted_string_list.append(s + ',') + + s2 = formatted_string_list.pop() + formatted_string_list.append(s2.rstrip(',')) + + formatted_string_list.append(last_substring) + + return " " .join(formatted_string_list) + + ## read json_content from work area + json_string = self.page.locator(LicenseTabLocators.JSON_ELEMENT).inner_text() + formatted_json_string = format_json_string(json_string) + expected_json_data = json.loads(formatted_json_string) + + # send request to backend to get license info + response = self.send_get_api_request("e-cmdb/api/lic") + response_body = self.get_response_body(response) + + ## temporarily + del response_body["netManagment"] + response_body["ui"].pop("lcc") + + self.assertion.check_json_equals(expected_json_data, response_body, \ + "Expected json content is not equal actual:") diff --git a/pages/login_page.py b/pages/login_page.py new file mode 100644 index 0000000..0cb736b --- /dev/null +++ b/pages/login_page.py @@ -0,0 +1,59 @@ +from pages.base_page import BasePage +from data.constants import Constants +from Locators.login import LoginPageLocators +from data.assertions import Assertions +from data.environment import host +from playwright.sync_api import Page + +class LoginPage(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.token = "" + self.assertion = Assertions(page) + + def do_login(self, username: str = None, password: str = None): + """Выполняет вход в систему. + Если username/password не указаны, использует значения из Constants""" + def handle_response(response): + if "login" in response.url: + response_body = self.get_response_body(response) + if response_body: + token = response_body.get("access_token") + host.set_access_token(token) + + self.page.on("response", handle_response) + + self.open("") + + # Используем переданные значения или значения по умолчанию из Constants + actual_username = username if username is not None else Constants.login + actual_password = password if password is not None else Constants.password + + self.clear_input(LoginPageLocators.USERNAME_INPUT) + self.input(LoginPageLocators.USERNAME_INPUT, actual_username) + + self.clear_input(LoginPageLocators.PASSWORD_INPUT) + self.input(LoginPageLocators.PASSWORD_INPUT, actual_password) + + self.click(LoginPageLocators.LOGIN_BTN) + + self.assertion.check_URL("dashboard", "An unexpected page has been opened") + + def do_unsuccessful_login(self, username: str = "someuser", password: str = "password"): + """Выполняет попытку входа с неверными учетными данными. + Можно передать свои неверные данные или использовать значения по умолчанию""" + incorrect_credentials_text = "Неверная пара логин/пароль" + + self.open("") + + self.clear_input(LoginPageLocators.USERNAME_INPUT) + self.input(LoginPageLocators.USERNAME_INPUT, username) + + self.clear_input(LoginPageLocators.PASSWORD_INPUT) + self.input(LoginPageLocators.PASSWORD_INPUT, password) + + self.click(LoginPageLocators.LOGIN_BTN) + + self.assertion.check_alert_window_with_text("error", incorrect_credentials_text) + + \ No newline at end of file diff --git a/pages/main_page.py b/pages/main_page.py new file mode 100644 index 0000000..cb42352 --- /dev/null +++ b/pages/main_page.py @@ -0,0 +1,42 @@ +from pages.base_page import BasePage +from Locators.main_page import MainPageLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +class MainPage(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) + + def should_be_navigation_panel(self): + self.assertion.check_presence(MainPageLocators.NAVIGATION_PANEL, "Navigation panel is not present") + + def click_main_navigation_panel_item(self, item_name): + button_locator = None + header_locator = None + + if item_name == "dashboard": + button_locator = MainPageLocators.NAVIGATION_PANEL_DASHBOARD_BUTTON + header_locator = MainPageLocators.NAVIGATION_PANEL_DASHBOARD_BUTTON_HEADER + elif item_name == "topology": + button_locator = MainPageLocators.NAVIGATION_PANEL_TOPOLOGY_BUTTON + header_locator = MainPageLocators.NAVIGATION_PANEL_TOPOLOGY_BUTTON_HEADER + elif item_name == "configuration": + button_locator = MainPageLocators.NAVIGATION_PANEL_CONFIGURATION_BUTTON + header_locator = MainPageLocators.NAVIGATION_PANEL_CONFIGURATION_BUTTON_HEADER + else: + assert False, "Unsupported main navigation panel item" + + self.click(button_locator) + self.assertion.check_URL(item_name, "An unexpected page has been opened") + self.assertion.check_element_active(header_locator, f"{item_name} button is not active") + + def do_logout(self): + self.click(MainPageLocators.CURRENT_USER_BUTTON) + + self.assertion.check_presence(MainPageLocators.CURRENT_USER_WINDOW, "Window with current user data has not been opened") + + logout_button_text = "Выйти" + self.page.get_by_role("button", name=logout_button_text).click() + + self.assertion.check_URL("login", "An unexpected page has been opened") diff --git a/pages/service_status_tab.py b/pages/service_status_tab.py new file mode 100644 index 0000000..4162504 --- /dev/null +++ b/pages/service_status_tab.py @@ -0,0 +1,27 @@ +from pages.base_page import BasePage +from Locators.configuration_page import ConfigurationPageLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +class ServiceStatusTab(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) + + def should_be_service_status_work_area(self): + self.assertion.have_title(ConfigurationPageLocators.WORK_AREA_TITLE, "Статус обслуживания", \ + "Expected work area page title is not equal real title") + + self.should_be_service_status_table() + + + def should_be_service_status_table(self): + headers = ['Контейнер', 'Время создания', 'Статус', 'Время работы', 'Image ID', 'Image ТЭГ'] + + service_status_table = self.read_table_data(ConfigurationPageLocators.WORK_AREA_TABLE) + + service_status_table_headers = service_status_table[0] + self.assertion.check_equals(service_status_table_headers, headers, \ + f"Expected table headers {service_status_table_headers} are not equal {headers}") + self.assertion.check_not_equals(len(service_status_table) - 1, 0, \ + "Service status table is empty") diff --git a/pages/session_tab.py b/pages/session_tab.py new file mode 100644 index 0000000..a3f32f6 --- /dev/null +++ b/pages/session_tab.py @@ -0,0 +1,221 @@ +from pages.base_page import BasePage +from Locators.session_locators import SessionLocators +from data.assertions import Assertions +from playwright.sync_api import Page, expect +import time + +class SessionTab(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertions = Assertions(page) + self.expected_headers = ['ID сессии', 'ID пользователя', 'Время жизни', 'Роль', 'Адрес'] + + def should_be_session_table(self) -> None: + """ Проверка таблицы сессий """ + + self.assertions.have_title( + SessionLocators.TEXT_TITLE, + 'Сессия', + "Заголовок страницы не соответствует ожидаемому" + ) + + # Проверка наименования заголовков таблицы сессий + session_table_data = self.read_table_data(SessionLocators.TABLE_BODY) + actual_headers = session_table_data[0] + + self.assertions.check_equals(actual_headers, self.expected_headers, "Заголовки таблицы не соответствуют ожидаемым") + + # Проверка, что таблица не пустая + self.assertions.check_not_equals(len(session_table_data) - 1, 0, "Таблица сессий пуста") + + # Проверка наличия всех необходимых колонок + required_columns = [ + SessionLocators.TABLE_HEADER_CELL_SESSION_ID, + SessionLocators.TABLE_HEADER_CELL_USER_ID, + SessionLocators.TABLE_HEADER_CELL_LIFETIME, + SessionLocators.TABLE_HEADER_CELL_ROLE, + SessionLocators.TABLE_HEADER_CELL_ADDRESS + ] + + for locator in required_columns: + self.is_element_present(locator, 1000) + + + def should_be_session_table_data_vs_api(self) -> None: + """ Проверка соответствие данных в таблице сессий с данными из API """ + ROLE_MAPPING = { + 'user': 'Пользователь', + 'administrator': 'Администратор' + } + + # Получение данных из UI + ui_session_id = self.get_text(SessionLocators.TABLE_HEADER_CELL_SESSION_ID, 0).strip() + ui_session_userId = self.get_text(SessionLocators.TABLE_HEADER_CELL_USER_ID, 0).strip() + ui_session_roles = self.get_text(SessionLocators.TABLE_HEADER_CELL_ROLE, 0).strip() + ui_session_ip = self.get_text(SessionLocators.TABLE_HEADER_CELL_ADDRESS, 0).strip() + + # Получение данных из API + response = self.send_get_api_request("e-nms/auth/sessions") + response_body = self.get_response_body(response) + + api_session_id = response_body[0].get('id') + api_session_userId = response_body[0].get('userId') + api_session_roles = response_body[0].get('roles', []) + api_session_ip = response_body[0].get('ip') + + # Преобразование и сортировка ролей из API + translated_roles = [ROLE_MAPPING.get(role.lower(), role) for role in api_session_roles] + api_session_roles_str = ', '.join(sorted(translated_roles)) + + # Сортировка ролей из UI для сравнения + ui_roles_sorted = ', '.join(sorted(role.strip() for role in ui_session_roles.split(','))) + + # Проверки соответствия данных + self.assertions.check_equals(ui_session_id, api_session_id, "ID сессии не совпадает") + self.assertions.check_equals(ui_session_userId, api_session_userId, "ID пользователя не совпадает") + self.assertions.check_equals(ui_roles_sorted, api_session_roles_str, "Роли не совпадают") + self.assertions.check_equals(ui_session_ip, api_session_ip, "IP-адрес не совпадает") + + # Логирование для отладки + #print(f"Session ID (UI/API): {ui_session_id}/{api_session_id}") + #print(f"User ID (UI/API): {ui_session_userId}/{api_session_userId}") + #print(f"Roles (UI/API): {ui_session_roles}/{api_session_roles}") + #print(f"IP Address (UI/API): {ui_session_ip}/{api_session_ip}") + + def should_be_new_session_added(self) -> None: + """ + Проверка добавленных записей в таблицу сессий + """ + + def should_be_vertical_scroll_session(self) -> None: + """ Проверка вертикального скролла таблицы сессий """ + + self.should_be_vertical_scroll(SessionLocators.TABLE_SCROLL_CONTAINER) + + + def open_last_session_modal(self) -> None: + """Открывает модальное окно удаления через последнюю строку таблицы сессий""" + print("\n=== Открытие модального окна через последнюю строку ===") + + # Ожидаем загрузки таблицы + self.page.wait_for_selector(SessionLocators.TABLE_BODY, state="visible", timeout=5000) + print("✓ Таблица сессий загружена") + + # Получаем все строки таблицы + rows = self.page.locator(SessionLocators.TABLE_ROWS) + row_count = rows.count() + + if row_count == 0: + raise AssertionError("Таблица сессий пуста, нечего удалять!") + + print(f"Всего строк в таблице: {row_count}") + + # Берём последнюю строку + last_row = rows.last + print("✓ Последняя строка получена") + + # Прокручиваем к последней строке (если нужно) + last_row.scroll_into_view_if_needed() + + # Локатор кнопки удаления в последней строке + delete_button = last_row.locator(SessionLocators.BUTTON_DELETE_SESSION) + + # Проверяем и кликаем кнопку удаления + expect(delete_button).to_be_visible(timeout=5000) + print("Нажимаем кнопку удаления в последней строке...") + delete_button.click() + + # Проверяем появление модального окна + modal = self.page.locator(SessionLocators.MODAL_WINDOW) + expect(modal).to_be_visible(timeout=5000) + print("✓ Модальное окно удаления успешно открыто") + + # Проверка заголовка окна + if hasattr(SessionLocators, 'MODAL_TITLE'): + modal_title = self.page.locator(SessionLocators.MODAL_TITLE) + expect(modal_title).to_contain_text("Удаление") + print("✓ Заголовок модального окна корректен") + + print("=== Модальное окно через последнюю строку открыто успешно ===\n") + + def should_be_horizontal_scroll_session_modal(self) -> None: + """ Проверка горизонтального скролла модального окна """ + self.should_be_horizontal_scroll() + + + def should_be_close_modal_window_by_button(self, button_type: str) -> None: + """ + Проверка кнопок модального окна подтверждения удаления сессии с детальным логированием + :param button_type: Тип кнопки ('close', 'cancel', 'delete') + """ + print(f"\n=== Начало проверки модального окна для кнопки '{button_type}' ===") + + # 1. Проверка видимости модального окна перед действиями + modal = self.page.locator(SessionLocators.MODAL_WINDOW) + expect(modal).to_be_visible() + print("✓ Модальное окно отображается перед взаимодействием") + + if button_type == 'close': + # 2. Нажатие кнопки закрытия (X) + print("Нажимаем кнопку закрытия (X)...") + close_button = self.page.locator(SessionLocators.MODAL_CLOSE_BUTTON) + close_button.click() + + # 3. Проверка закрытия окна + expect(modal).not_to_be_visible() + print("✓ Проверяем, что окно закрылось после нажатия 'X'") + + elif button_type == 'cancel': + # 2. Нажатие кнопки "Отмена" + print("Нажимаем кнопку 'Отмена'...") + cancel_button = self.page.locator(SessionLocators.MODAL_CANCEL_BUTTON) + cancel_button.click() + + # 3. Проверка закрытия окна + expect(modal).not_to_be_visible() + print("✓ Проверяем, что окно закрылось после нажатия 'Отмена'") + + elif button_type == 'delete': + # 2. Подготовка к удалению + rows_before = self.page.locator(SessionLocators.TABLE_ROWS).count() + print(f"Количество строк до удаления: {rows_before}") + + # 3. Нажатие кнопки "Удалить" + print("Нажимаем кнопку 'Удалить'...") + delete_button = self.page.locator(SessionLocators.MODAL_DELETE_BUTTON) + delete_button.click() + """ + # 4. Проверка сообщения об успехе + success_msg = self.page.locator(SessionLocators.SUCCESS_MESSAGE) + expect(success_msg).to_be_visible(timeout=5000) + expect(success_msg).to_contain_text("Сессия успешно удалена") + print("✓ Сообщение об успешном удалении отображается") + """ + # 5. Проверка закрытия окна + expect(modal).not_to_be_visible() + print("✓ Проверяем, что окно закрылось после нажатия 'Удалить'") + """ + # 6. Проверка исчезновения сообщения (если нужно) + expect(success_msg).not_to_be_visible(timeout=10000) + print("✓ Сообщение автоматически скрылось (если предусмотрено UI)") + """ + # 7. Проверка изменения таблицы + rows_after = self.page.locator(SessionLocators.TABLE_ROWS).count() + self.assertions.check_equals(rows_after, rows_before - 1, + "Количество строк не изменилось после удаления") + print(f"✓ Количество строк после удаления: {rows_after} (уменьшилось на 1)") + + print(f"=== Проверка для кнопки '{button_type}' успешно завершена ===\n") + + + def delete_all_created_sessions(self, username) -> None: + """ Проверка удаления созданных ссесий """ + + def delete_current_session(self) -> None: + """ Проверка удаления текущей сессии """ + + + + + + \ No newline at end of file diff --git a/pages/users_tab.py b/pages/users_tab.py new file mode 100644 index 0000000..2f909a0 --- /dev/null +++ b/pages/users_tab.py @@ -0,0 +1,432 @@ +from pages.base_page import BasePage +from Locators.base_page import BasePageLocators +from Locators.configuration_page import ConfigurationPageLocators +from Locators.users_tab import UsersTabLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +import re + +class UsersTab(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) + self.role_dict = {"administrator": "Администратор", + "manager":"Контактное лицо", + "operator":"Оператор", + "inform_secur_user" : "Специалист информационной безопасности"} + + def add_new_user(self, user_data): + fields = user_data.keys() + + self.assertion.check_presence(UsersTabLocators.USER_DATA_INPUT_FORM, \ + "Input form for add new user is not present") + + ## input user name + if "name" in fields: + loc = "xpath=div[2]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["name"]) + + + ## input user role + if "role" in fields: + loc = "xpath=div[3]/div[2]/div/div/div/div/div[1]" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).click() + + self.assertion.check_presence(UsersTabLocators.USER_DATA_INPUT_FORM_ROLES_MENU, \ + "Roles drop-down menu is not present") + role = user_data["role"] + self.assertion.check_menu_item_with_text(role, f"No menu item with text: {role}") + self.page.get_by_role("listitem").filter(has_text=role).click() + + ## input password + if "password" in fields: + loc = "xpath=div[4]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["password"]) + + ## input commentary + if "commentary" in fields: + loc = "xpath=div[5]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["commentary"]) + + ## input e-mail + if "email" in fields: + loc = "xpath=div[6]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["email"]) + + ## input phone number for sms + if "phone_number" in fields: + loc = "xpath=div[7]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["phone_number"]) + + ##to be done: checkbox + + ## click add user button + add_button_text = "Добавить" + self.assertion.check_button_presence_with_text(add_button_text, f"Add user input form button with text {add_button_text} is not present") + self.page.get_by_role("button", name=add_button_text).click() + + ##check add user confirmation dialog + confirm_add_button_text = " Добавить " + self.assertion.check_confirmation_dialog_with_title(UsersTabLocators.USER_ACTION_CONFIRMATION_DIALOG, "Добавить нового пользователя") + self.page.get_by_role("button", name=confirm_add_button_text).first.click() + + ## check message about successfull user addition + self.assertion.check_alert_window_with_text("success", ' Новый пользователь \n успешно добавлен! ') + + + def close_user_window_by_toolbar_button(self, window_title): + BUTTON_CLOSE_ON_TOOLBAR = f"xpath=(.//*[normalize-space(text()) and normalize-space(.)='{window_title}'])[1]/following::*[name()='svg'][1]" + self.assertion.check_presence(BUTTON_CLOSE_ON_TOOLBAR, \ + "Close button is not presenet on toolbar") + self.click(BUTTON_CLOSE_ON_TOOLBAR) + + self.assertion.check_absence(UsersTabLocators.USER_DATA_WORK_AREA_TITLE, \ + f"{window_title} window should be closed") + + def close_user_window(self, window_title): + close_button_text = "Закрыть" + self.assertion.check_button_presence_with_text(close_button_text, f"Edit user input form button with text {close_button_text} is not present") + self.page.get_by_role("button", name=close_button_text).click() + + self.assertion.check_absence(UsersTabLocators.USER_DATA_WORK_AREA_TITLE, \ + f"{window_title} window should be closed") + + def delete_user(self): + remove_button_text = "Удалить" + self.assertion.check_button_presence_with_text(remove_button_text, f"Edit user input form button with text {remove_button_text} is not present") + self.page.get_by_role("button", name=remove_button_text).click() + + ##check add user confirmation dialog + confirm_remove_button_text = " Удалить " + self.assertion.check_confirmation_dialog_with_title(UsersTabLocators.USER_ACTION_CONFIRMATION_DIALOG, "Удаление") + self.page.get_by_role("button", name=confirm_remove_button_text).first.click() + + ## check message about successfull user deletion + self.assertion.check_alert_window_with_text("success", '\nПользователь удалён\n') + + def edit_user(self, user_data): + fields = user_data.keys() + + self.assertion.check_presence(UsersTabLocators.USER_DATA_INPUT_FORM, \ + "Input form for edit user is not present") + + ## input user name + if "name" in fields: + loc = "xpath=div[2]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["name"]) + + + ## input user role + if "role" in fields: + loc = "xpath=div[3]/div[2]/div/div/div/div/div[1]" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).click() + + self.assertion.check_presence(UsersTabLocators.USER_DATA_INPUT_FORM_ROLES_MENU, \ + "Roles drop-down menu is not present") + role = user_data["role"] + self.assertion.check_menu_item_with_text(role, f"No menu item with text: {role}") + self.page.get_by_role("listitem").filter(has_text=role).click() + + ## input commentary + if "commentary" in fields: + loc = "xpath=div[5]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["commentary"]) + + ## input e-mail + if "email" in fields: + loc = "xpath=div[5]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["email"]) + + ## input phone number for sms + if "phone_number" in fields: + loc = "xpath=div[6]/div[2]/div/div/div/div/input" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).fill(user_data["phone_number"]) + + ##to be done: checkbox + + ## click add user button + add_button_text = "Сохранить" + self.assertion.check_button_presence_with_text(add_button_text, f"Add user input form button with text {add_button_text} is not present") + self.page.get_by_role("button", name=add_button_text).click() + + ##check add user confirmation dialog + confirm_add_button_text = " Сохранить " + self.assertion.check_confirmation_dialog_with_title(UsersTabLocators.USER_ACTION_CONFIRMATION_DIALOG, "Сохранение") + self.page.get_by_role("button", name=confirm_add_button_text).first.click() + + ## check message about successfull user addition. Temporarily without translation + self.assertion.check_alert_window_with_text("success", '\nupdate success\n') + + + def open_add_user_page(self): + self.assertion.check_presence(UsersTabLocators.TOOLBAR_EDIT_BUTTON, \ + "Edit button is not presenet on toolbar") + self.click(UsersTabLocators.TOOLBAR_EDIT_BUTTON) + self.assertion.check_presence(UsersTabLocators.TOOLBAR_ADD_USER_BUTTON, \ + "Add User button is not presenet on toolbar") + + self.click(UsersTabLocators.TOOLBAR_ADD_USER_BUTTON) + + # check that new work area with title has been opened + self.assertion.have_title(UsersTabLocators.USER_DATA_WORK_AREA_TITLE, "Добавить нового пользователя", \ + "Expected work area page title is not equal real title") + + def open_edit_user_page_by_index(self, row_index): + ## temporarily + tmp_dict = {"admin":"Администратор", "manager":"Контактное лицо", "operator":"Оператор"} + + users_table = self.read_table_data(ConfigurationPageLocators.WORK_AREA_TABLE) + self.assertion.check_not_equals(len(users_table) - 1, 0, "Users table is empty") + + # remove header + del users_table[0] + + # check row_index + if row_index > len(users_table): + assert False, "Row_index is out of range" + + # get user name and role + user_name = users_table[row_index][0] + for key, val in tmp_dict.items(): + if user_name == val: + user_name = key + + role = users_table[row_index][2] + + # click to found table row + self.page.locator(ConfigurationPageLocators.WORK_AREA_TABLE).locator(BasePageLocators.TABLE_BODY).nth(row_index).click() + + # check that edit user work area with user name title has been opened + self.assertion.have_title(UsersTabLocators.USER_DATA_WORK_AREA_TITLE, user_name, \ + "Expected edit user work area page title is not equal real title") + return user_name, role + + def reset_password(self): + new_password = "" + + reset_password_button_text = "Сбросить пароль" + self.assertion.check_button_presence_with_text(reset_password_button_text, f"Edit user input form button with text {reset_password_button_text} is not present") + self.page.get_by_role("button", name=reset_password_button_text).click() + + alert_message = self.get_text(BasePageLocators.ALERT_WINDOW_TEXT_SUCCESS, 0) + if len(alert_message) > 0: + new_password = re.findall(r'[\d]+', alert_message)[0] + + return new_password + + def open_edit_user_page_by_name(self, name, role): + row_index = self.find_user_in_table(name, role) + if row_index == -1: + assert False, f"User with name {name} and role {role} has not been found" + + # click to found table row + self.page.locator(ConfigurationPageLocators.WORK_AREA_TABLE).locator(BasePageLocators.TABLE_BODY).nth(row_index).click() + + # check that edit user work area with user name title has been opened + self.assertion.have_title(UsersTabLocators.USER_DATA_WORK_AREA_TITLE, name, \ + "Expected edit user work area page title is not equal real title") + + def should_be_users_work_area(self): + self.assertion.have_title(ConfigurationPageLocators.WORK_AREA_TITLE, "Пользователи", \ + "Expected work area page title is not equal real title") + + self.assertion.check_presence(UsersTabLocators.TOOLBAR_EDIT_BUTTON, \ + "Edit button is not presenet on toolbar") + self.should_be_users_table() + + def should_be_users_page_toolbar_buttons(self): + self.assertion.check_presence(UsersTabLocators.TOOLBAR_EDIT_BUTTON, \ + "Edit button is not presenet on toolbar") + self.assertion.check_tooltip_with_text(UsersTabLocators.TOOLBAR_EDIT_BUTTON, "Редактировать") + + self.click(UsersTabLocators.TOOLBAR_EDIT_BUTTON) + self.assertion.check_presence(UsersTabLocators.TOOLBAR_ADD_USER_BUTTON, \ + "Add User button is not presenet on toolbar") + self.assertion.check_presence(UsersTabLocators.TOOLBAR_CLOSE_BUTTON, \ + "Close button is not presenet on toolbar") + + self.assertion.check_tooltip_with_text(UsersTabLocators.TOOLBAR_ADD_USER_BUTTON, "Добавить") + self.assertion.check_tooltip_with_text(UsersTabLocators.TOOLBAR_CLOSE_BUTTON, "Закрыть") + + self.click(UsersTabLocators.TOOLBAR_CLOSE_BUTTON) + self.assertion.check_presence(UsersTabLocators.TOOLBAR_EDIT_BUTTON, \ + "Edit button is not presenet on toolbar") + + def should_be_add_user_work_area(self): + self.assertion.have_title(UsersTabLocators.USER_DATA_WORK_AREA_TITLE, "Добавить нового пользователя", \ + "Expected add user window title is not equal real title") + self.assertion.check_presence(UsersTabLocators.ADD_USER_WORK_AREA_CLOSE_BUTTON, \ + "Close button is not presenet on toolbar") + self.assertion.check_tooltip_with_text(UsersTabLocators.ADD_USER_WORK_AREA_CLOSE_BUTTON, "Закрыть") + + # check input form + self.should_be_add_user_input_form() + + # check input form buttons + add_button_text = "Добавить" + self.assertion.check_button_presence_with_text(add_button_text, f"Add user input form button with text {add_button_text} is not present") + + close_button_text = "Закрыть" + self.assertion.check_button_presence_with_text(close_button_text, f"Add user input form button with text {close_button_text} is not present") + + def should_be_edit_user_work_area(self, user_name, role): + EDIT_USER_WORK_AREA_CLOSE_BUTTON = f"xpath=(.//*[normalize-space(text()) and normalize-space(.)='{user_name}'])[1]/following::*[name()='svg'][1]" + + self.assertion.have_title(UsersTabLocators.USER_DATA_WORK_AREA_TITLE, f"{user_name}", \ + "Expected edit user window title is not equal real title") + + self.assertion.check_presence(EDIT_USER_WORK_AREA_CLOSE_BUTTON, \ + "Close button is not presenet on toolbar") + self.assertion.check_tooltip_with_text(EDIT_USER_WORK_AREA_CLOSE_BUTTON, "Закрыть") + + # check edit user input form + self.should_be_edit_user_input_form(user_name, role) + + # check input form buttons + save_button_text = "Сохранить" + self.assertion.check_button_presence_with_text(save_button_text, f"Edit user input form button with text {save_button_text} is not present") + + remove_button_text = "Удалить" + self.assertion.check_button_presence_with_text(remove_button_text, f"Edit user input form button with text {remove_button_text} is not present") + + reset_password_button_text = "Сбросить пароль" + self.assertion.check_button_presence_with_text(reset_password_button_text, f"Edit user input form button with text {reset_password_button_text} is not present") + + close_button_text = "Закрыть" + self.assertion.check_button_presence_with_text(close_button_text, f"Edit user input form button with text {close_button_text} is not present") + + def should_be_users_table(self): + headers = ['Имя пользователя', 'hash_password', 'Роль', 'E-mail', 'Номер для СМС'] + + users_table = self.read_table_data(ConfigurationPageLocators.WORK_AREA_TABLE) + + users_table_headers = users_table[0] + self.assertion.check_equals(users_table_headers, headers, \ + f"Expected table headers {users_table_headers} are not equal {headers}") + self.assertion.check_not_equals(len(users_table) - 1, 0, \ + "Users table is empty") + self.verify_users_table_content(users_table) + + def should_be_add_user_input_form(self): + self.assertion.check_presence(UsersTabLocators.USER_DATA_INPUT_FORM, \ + "Input form for add new user is not present") + + n = 7 + for i in range (1, n + 1): + loc = f"xpath=div[{i}]/div[2]/div/div/div/div/input" + input_area = self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc) + if i == 1: + checked = self.page.get_by_role("checkbox").first.is_checked() + self.assertion.check_equals(checked, False, "Checkbox is checked by default") + + loc = f"xpath={UsersTabLocators.USER_DATA_INPUT_FORM}/div[1]/div[2]/div/div/div{UsersTabLocators.USER_DATA_INPUT_FORM_NOTIFICATION_LABEL}" + self.assertion.have_text(loc, "ad", \ + "Label for ad is not present") + elif i == 3: + loc = f"xpath=div[{i}]/div[2]/div/div/div/div/div[1]" + self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).click() + + self.assertion.check_presence(UsersTabLocators.USER_DATA_INPUT_FORM_ROLES_MENU, \ + "Roles drop-down menu is not present") + roles = self.role_dict.values() + + for role in roles: + self.assertion.check_menu_item_with_text(role, f"No menu item with text: {role}") + else: + self.assertion.check_empty_input_area(input_area, "No empty input area") + + checked = self.page.get_by_role("checkbox").nth(1).is_checked() + self.assertion.check_equals(checked, False, "Checkbox is checked by default") + + loc = f"xpath={UsersTabLocators.USER_DATA_INPUT_FORM}/div[8]/div/div/div/div{UsersTabLocators.USER_DATA_INPUT_FORM_NOTIFICATION_LABEL}" + self.assertion.have_text(loc, "Подписка на Push-уведомления", \ + "Label for push-notifications subscribtion is not present") + + def should_be_edit_user_input_form(self, user_name, role): + self.assertion.check_presence(UsersTabLocators.USER_DATA_INPUT_FORM, \ + "Input form for edit user is not present") + + loc = "xpath=div[2]/div[2]/div/div/div/div/input" + text_value = self.page.locator(UsersTabLocators.USER_DATA_INPUT_FORM).locator(loc).input_value() + + + self.assertion.check_equals(text_value, user_name, "Expected user name is not equal real user name") + + self.assertion.check_menu_item_with_text(role, f"No menu item with text: {role}") + + def should_be_user_in_table(self, name, role): + found = self.find_user_in_table(name, role) + if found == -1: + assert False, f"User with name {name} and role {role} has not been found" + + def should_not_be_user_in_table(self, name, role): + found = self.find_user_in_table(name, role) + if found != -1: + assert False, f"User with name {name} and role {role} has been found" + + def find_user_in_table(self, name, role): + users_table = self.read_table_data(ConfigurationPageLocators.WORK_AREA_TABLE) + self.assertion.check_not_equals(len(users_table) - 1, 0, "Users table is empty") + + # remove header + del users_table[0] + + not_found_index = -1 + row_index = 0 + + for user_info in users_table: + if name in user_info and role in user_info: + return row_index + row_index += 1 + return not_found_index + + def verify_users_table_content(self, users_table): + expected_users_list = [] + + ## temporarily + tmp_dict = {"admin":"Администратор", "manager":"Контактное лицо", "operator":"Оператор"} + + query = {"id": ["/catalogs/user"], "data": {"namePath": True, "children": {"flatten": True}}} + + response = self.send_post_api_request("e-cmdb/api/query", query) + response_body = self.get_response_body(response) + + for item in response_body[0]["children"]: + user_info = [] + + ## temporarily + user_name = item["name"] + if user_name in tmp_dict.keys(): + item["name"] = tmp_dict[user_name] + + user_info.append(item["name"]) + if item["password"] is not None: + user_info.append(item["password"]) + else: + user_info.append("") + if item["role"] is not None: + role = item["role"] + if role in self.role_dict.keys(): + item["role"] = self.role_dict[role] + user_info.append(item["role"]) + else: + user_info.append("") + if item["email"] is not None: + user_info.append(item["email"]) + else: + user_info.append("") + if item["sms_phone"] is not None: + user_info.append(item["sms_phone"]) + else: + user_info.append("") + + expected_users_list.append(user_info) + + # remove header + del users_table[0] + + self.assertion.check_lists_equals(users_table, expected_users_list, \ + "Actual users list is not equal users list from db") + + diff --git a/pages/ztp_configuration_tab.py b/pages/ztp_configuration_tab.py new file mode 100644 index 0000000..e756a82 --- /dev/null +++ b/pages/ztp_configuration_tab.py @@ -0,0 +1,9 @@ +from pages.base_page import BasePage +# from Locators.ztp_tab import ZTPTabLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +class ZTPConfigurationTab(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) diff --git a/pages/ztp_templates_tab.py b/pages/ztp_templates_tab.py new file mode 100644 index 0000000..ce1809e --- /dev/null +++ b/pages/ztp_templates_tab.py @@ -0,0 +1,9 @@ +from pages.base_page import BasePage +# from Locators.ztp_tab import ZTPTabLocators +from data.assertions import Assertions +from playwright.sync_api import Page + +class ZTPTemplatesTab(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page) + self.assertion = Assertions(page) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e167e42 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +markers = + develop: current test development +addopts = -v -s \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2a57ba0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pytest +playwright +requests +qase-pytest==4.2.0 +python-dotenv +jsondiff + diff --git a/tests/__pycache__/test_session_tab.cpython-313-pytest-8.3.5.pyc b/tests/__pycache__/test_session_tab.cpython-313-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8f64fbdb1e6cd7ee2b95043ff1ea64483e6a793 GIT binary patch literal 5418 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