commit 701fd59bb415bdbe91c7206494cee2f3cffe40b0 Author: E41222052 Date: Sun Jun 7 22:57:40 2026 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8a28b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Virtual Environment +venv/ + +# Python +__pycache__/ +*.pyc + +# Pytest +.pytest_cache/ + +# Selenium +chromedriver* +screenshots/ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..a430f34 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Selenium Automated Testing Project + +Project ini digunakan untuk pengujian otomatis website +Adaptive Metacognitive Hypermedia Learning Environment (AMetative HLE) +menggunakan Selenium WebDriver dan framework pytest. + +## Tools & Tech Stack +- Python 3.x +- Selenium WebDriver +- Pytest +- WebDriver Manager + +## Cara Menjalankan Test +1. Clone repository +2. Install dependencies + pip install -r requirements.txt +3. Jalankan test + pytest -v + +## Jenis Pengujian +- Functional Testing +- Integration Testing +- Automated Regression Test + +## Author +Rurin Haliza + diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..b13c19b --- /dev/null +++ b/conftest.py @@ -0,0 +1,224 @@ +import os +import pytest +from datetime import datetime +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options + +from pages.register_page import RegisterPage +from utils.data_generator import generate_valid_register_data +from pages.login_page import LoginPage +from pages.dashboard_page import DashboardPage + + +# =============================== +# FIXTURE DRIVER +# =============================== +@pytest.fixture(scope="function") +def driver(): + options = Options() + options.add_argument("--start-maximized") + + service = Service() + driver = webdriver.Chrome(service=service, options=options) + + yield driver + driver.quit() + +# =============================== +# FIXTURE REGISTER PAGE +# =============================== +@pytest.fixture +def register_page(driver): + page = RegisterPage(driver) + page.open() + return page + +# =============================== +# DATA REGISTER VALID (FINAL) +# =============================== +@pytest.fixture +def valid_register_data(): + """ + - Data selalu VALID + - Data SELALU BARU setiap test + - Field boleh dioverride di test + """ + data = generate_valid_register_data() + + # pastikan field WAJIB selalu ada + data.setdefault("nama_lengkap", data["nama"]) + data.setdefault("konfirmasi_password", data["password"]) + + return data + +# =============================== +# SCREENSHOT SAAT FAIL +# =============================== +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + """ + Ambil screenshot otomatis jika test FAIL + """ + outcome = yield + rep = outcome.get_result() + + if rep.when == "call" and rep.failed: + driver = item.funcargs.get("driver") + if driver: + screenshots_dir = "screenshots" + os.makedirs(screenshots_dir, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + file_path = os.path.join( + screenshots_dir, + f"{item.name}_{timestamp}.png" + ) + + driver.save_screenshot(file_path) + print(f"\nšŸ“ø Screenshot saved: {file_path}") + +# =============================== +# FIXTURE LOGIN PAGE +# =============================== +@pytest.fixture +def login_page(driver): + page = LoginPage(driver) + page.open() + return page + +# ========================= +# LOGIN FIXTURES FOR TEST +# ========================= + +@pytest.fixture +def login_as_user_belum_kuesioner(driver): + """ + User valid, BELUM pernah mengisi kuesioner + Digunakan untuk: + - popup dashboard + - status KM / RM kosong + """ + + login_page = LoginPage(driver) + login_page.open() + + login_page.fill_email("e41222052@student.polije.ac.id") + login_page.fill_password("e41222052@student.polije.ac.id") + login_page.submit() + + dashboard = DashboardPage(driver) + dashboard.open() + + return driver + +@pytest.fixture +def login_as_user_sudah_kuesioner(driver): + """ + User valid, SUDAH pernah mengisi kuesioner + Digunakan untuk: + - status KM / RM tersedia + - tidak ada popup + """ + + login_page = LoginPage(driver) + login_page.open() + + # Akun yang SUDAH isi kuesioner + login_page.fill_email("tester@polije.ac.id") + login_page.fill_password("tester@polije.ac.id") + login_page.submit() + + dashboard = DashboardPage(driver) + dashboard.open() + + return driver + +@pytest.fixture +def login_as_user_belum_kuesioner2(driver): + """ + User valid, SUDAH pernah mengisi kuesioner + Digunakan untuk: + - status KM / RM tersedia + - tidak ada popup + """ + + login_page = LoginPage(driver) + login_page.open() + + # Akun yang SUDAH isi kuesioner + login_page.fill_email("akuntesting2@polije.ac.id") + login_page.fill_password("akuntesting2@polije.ac.id") + login_page.submit() + + dashboard = DashboardPage(driver) + dashboard.open() + + return driver + +#Account For Regression +@pytest.fixture +def login_as_user_belum_kuesionerNew(driver): + """ + User valid, SUDAH pernah mengisi kuesioner + Digunakan untuk: + - status KM / RM tersedia + - tidak ada popup + """ + + login_page = LoginPage(driver) + login_page.open() + + # Akun yang SUDAH isi kuesioner + login_page.fill_email("tester2@polije.ac.id") + login_page.fill_password("tester2@polije.ac.id") + login_page.submit() + + dashboard = DashboardPage(driver) + dashboard.open() + + return driver + +@pytest.fixture +def login_as_user_belum_kuesionerNew2(driver): + """ + User valid, SUDAH pernah mengisi kuesioner + Digunakan untuk: + - status KM / RM tersedia + - tidak ada popup + """ + + login_page = LoginPage(driver) + login_page.open() + + # Akun yang SUDAH isi kuesioner + login_page.fill_email("iniemail@polije.ac.id") + login_page.fill_password("inipassword") + login_page.submit() + + dashboard = DashboardPage(driver) + dashboard.open() + + return driver + +@pytest.fixture +def login_as_user_sudah_kuesioner3(driver): + """ + User valid, SUDAH pernah mengisi kuesioner + Digunakan untuk: + - status KM / RM tersedia + - tidak ada popup + """ + + login_page = LoginPage(driver) + login_page.open() + + # Akun yang SUDAH isi kuesioner + login_page.fill_email("sudah@polije.ac.id") + login_page.fill_password("sudah@polije.ac.id") + login_page.submit() + + dashboard = DashboardPage(driver) + dashboard.open() + + return driver diff --git a/pages/dashboard_page.py b/pages/dashboard_page.py new file mode 100644 index 0000000..dfd866b --- /dev/null +++ b/pages/dashboard_page.py @@ -0,0 +1,206 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import re + + +class DashboardPage: + URL = "https://hypermedialearning.project2025.id/public/dashboard" + + # ===== POPUP (MODAL) ===== + POPUP_MODAL = (By.ID, "kuesionerModal") + + BTN_ISI_KUESIONER_POPUP = ( + By.XPATH, "//div[@id='kuesionerModal']//a[contains(text(),'Isi Kuesioner')]" + ) + + BTN_TUTUP_POPUP = ( + By.XPATH, "//div[@id='kuesionerModal']//button[contains(text(),'Tutup')]" + ) + + BTN_CLOSE_X = ( + By.XPATH, "//div[@id='kuesionerModal']//button[@aria-label='Close']" + ) + + MODAL_KUESIONER = (By.ID, "kuesionerModal") + + BTN_TUTUP_MODAL = ( + By.XPATH, + "//button[contains(text(),'Nanti') or contains(text(),'Tutup')]" + ) + + KM_VALUE = ( + By.XPATH, + "//span[normalize-space()='Knowledge of Metakognitif (KM)']" + "/ancestor::div[contains(@class,'card')]" + "//span[normalize-space()='Nilai']/following-sibling::span" +) + + RM_VALUE = ( + By.XPATH, + "//span[normalize-space()='Regulation of Metakognitif (RM)']" + "/ancestor::div[contains(@class,'card')]" + "//span[normalize-space()='Nilai']/following-sibling::span" +) + + LEARNING_STYLE_VALUE = ( + By.XPATH, + "//span[normalize-space()='Learning Style']" + "/ancestor::div[contains(@class,'card')]" + "//span[normalize-space()='Gaya Belajar']/following-sibling::span" +) + + PENGISIAN_KUESIONER_SECTION = ( + By.XPATH, + "//h6[normalize-space()='Pengisian Kuesioner']/ancestor::div[contains(@class,'card')]" +) + + KUESIONER_VARK_MAI_TEXT = ( + By.XPATH, + "//td[normalize-space()='Kuesioner VARK dan MAI']" +) + + ISI_KUESIONER_BUTTON = ( + By.XPATH, + "//a[@href='https://hypermedialearning.sanggadewa.my.id/kuesioner-panduan']" +) + + + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 15) + + def open(self): + self.driver.get(self.URL) + +# POM for test dashboard PopUp + + def is_popup_visible(self): + try: + self.wait.until( + EC.visibility_of_element_located(self.POPUP_MODAL) + ) + return True + except: + return False + + def click_isi_kuesioner_pop_up(self): + self.wait.until( + EC.element_to_be_clickable(self.BTN_ISI_KUESIONER_POPUP) + ).click() + + def close_popup_with_x(self): + btn = self.wait.until( + EC.visibility_of_element_located(self.BTN_CLOSE_X) + ) + btn.click() + self.wait_popup_disappear() + + def is_redirected_to_kuesioner(self): + self.wait.until(lambda d: "kuesioner" in d.current_url) + return True + + def wait_popup_disappear(self): + self.wait.until( + EC.invisibility_of_element_located(self.POPUP_MODAL) + ) + +#POM for Test Dashboard KMRM + + def close_kuesioner_popup_if_present(self): + btn = self.wait.until( + EC.visibility_of_element_located(self.BTN_CLOSE_X) + ) + btn.click() + self.wait_popup_disappear() + + def get_km_status_text(self): + return self.wait.until( + EC.visibility_of_element_located(self.KM_VALUE) + ).text.strip() + + def get_rm_status_text(self): + return self.wait.until( + EC.visibility_of_element_located(self.RM_VALUE) + ).text.strip() + + def is_km_not_filled(self): + return self.get_km_status_text() == "()" + + def is_rm_not_filled(self): + return self.get_rm_status_text() == "()" + + def is_km_filled(self): + el = self.wait.until( + EC.visibility_of_element_located(self.KM_VALUE) + ) + return el.text.strip() + + def is_rm_filled(self): + el = self.wait.until( + EC.visibility_of_element_located(self.RM_VALUE) + ) + return el.text.strip() + +#POM for Learning Style + + def get_learning_style_text(self) -> str: + el = self.wait.until( + EC.visibility_of_element_located(self.LEARNING_STYLE_VALUE) + ) + return el.text.strip() + + def is_learning_style_not_filled(self) -> bool: + return self.get_learning_style_text() == "(Tidak diketahui)" + + def is_learning_style_filled(self) -> bool: + text = self.get_learning_style_text() + return text != "" and text != "(Tidak diketahui)" + +#POM for Pengisian Kuesioner + def is_pengisian_kuesioner_section_visible(self) -> bool: + try: + self.wait.until( + EC.visibility_of_element_located( + self.PENGISIAN_KUESIONER_SECTION + ) + ) + return True + except: + return False + + def is_kuesioner_vark_mai_visible(self) -> bool: + try: + self.wait.until( + EC.visibility_of_element_located( + self.KUESIONER_VARK_MAI_TEXT + ) + ) + return True + except: + return False + + def click_isi_kuesioner(self): + self.wait.until( + EC.element_to_be_clickable( + self.ISI_KUESIONER_BUTTON + ) + ).click() + + + + + + + + + + + + + + + + + diff --git a/pages/history_kuesioner_page.py b/pages/history_kuesioner_page.py new file mode 100644 index 0000000..f3f5619 --- /dev/null +++ b/pages/history_kuesioner_page.py @@ -0,0 +1,186 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class HistoryKuesionerPage: + + URL = "https://hypermedialearning.sanggadewa.my.id/history_quis" + + # ===== SIDEBAR ===== + SIDEBAR_HISTORY = ( + By.XPATH, + "//li[contains(@class,'menu-item') and .//div[normalize-space()='History Kuisioner']]" + ) + + # ===== SKOR GAYA BELAJAR ===== + SCORE_CONTAINER = (By.CSS_SELECTOR, "div.card") + SCORE_LABELS = (By.CSS_SELECTOR, "div.score-label") + SCORE_VALUES = (By.CSS_SELECTOR, "div.score-value") + + LEARNING_STYLE_CARD = ( + By.XPATH, + "//div[contains(@class,'card') and .//div[text()='Visual']]" + ) + + LEARNING_STYLE_LABELS = ( + By.XPATH, + "//div[contains(@class,'card') and .//div[text()='Visual']]//div[@class='score-label']" + ) + + LEARNING_STYLE_VALUES = ( + By.XPATH, + "//div[contains(@class,'card') and .//div[text()='Visual']]//div[@class='score-value']" + ) + + LEARNING_STYLE_ITEMS = ( + By.CSS_SELECTOR, + "div.card.shadow.border-0.p-4 > div.row.text-center > div.col-6.col-md-3" + ) + + # ===== PROGRESS ===== + PROGRESS_BAR = (By.CSS_SELECTOR, "div.progress-bar") + PROGRESS_TEXT = ( + By.XPATH, + "//small[contains(text(),'pertanyaan dijawab')]" + ) + + # ===== GAYA BELAJAR DOMINAN ===== + DOMINANT_STYLE_TEXT = ( + By.XPATH, + "//h2[contains(@class,'fw-bold')]" + ) + + BTN_MULAI_BELAJAR = ( + By.XPATH, + "//a[contains(@href,'/materi') and contains(.,'Mulai')]" + ) + + # ===== KM & RM ===== + KM_VALUE = ( + By.XPATH, + "//div[normalize-space()='KM']/following-sibling::div[contains(@class,'score-value')]" + ) + + RM_VALUE = ( + By.XPATH, + "//div[normalize-space()='RM']/following-sibling::div[contains(@class,'score-value')]" + ) + + # ===== RIWAYAT ===== + TABLE_ROWS = (By.CSS_SELECTOR, "table.table tbody tr") + + # ===== AKSI ===== + BTN_MULAI_BELAJAR = ( + By.XPATH, + "//a[contains(@href,'/materi')]" + ) + + BTN_UBAH = ( + By.XPATH, + "//a[contains(@href,'kuesioner')]" + ) + + BTN_UNDUH = ( + By.XPATH, + "//a[contains(@href,'user-result')]" + ) + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + def open(self): + self.driver.get(self.URL) + + def is_page_loaded(self): + self.wait.until(EC.presence_of_element_located(self.SIDEBAR_HISTORY)) + return True + + def get_scores(self): + """ + Return skor gaya belajar saja. + PASSED = benar-benar 4 gaya belajar. + """ + items = self.wait.until( + lambda d: d.find_elements(*self.LEARNING_STYLE_ITEMS) + ) + + assert len(items) == 4, f"Expected 4 learning styles, found {len(items)}" + + scores = {} + for item in items: + label = item.find_element(By.CSS_SELECTOR, ".score-label").text.strip() + value = item.find_element(By.CSS_SELECTOR, ".score-value").text.strip() + scores[label] = value + + return scores + + def get_score_labels(self): + # tunggu card halaman muncul dulu + self.wait.until( + EC.presence_of_element_located(self.SCORE_CONTAINER) + ) + + # baru ambil labels + self.wait.until( + lambda d: len(d.find_elements(*self.SCORE_LABELS)) >= 4 + ) + + return [ + e.text.strip() + for e in self.driver.find_elements(*self.SCORE_LABELS) + if e.text.strip() != "" + ] + + def get_score_values(self): + self.wait.until( + lambda d: all( + e.text.strip().isdigit() + for e in d.find_elements(*self.LEARNING_STYLE_VALUES) + ) + ) + return [e.text.strip() for e in self.driver.find_elements(*self.LEARNING_STYLE_VALUES)] + + def get_progress_value(self): + bar = self.wait.until(EC.presence_of_element_located(self.PROGRESS_BAR)) + return bar.get_attribute("aria-valuenow") + + def get_progress_text(self): + return self.driver.find_element(*self.PROGRESS_TEXT).text + + def get_dominant_style(self): + return self.wait.until( + EC.presence_of_element_located(self.DOMINANT_STYLE_TEXT) + ).text.strip() + + def click_mulai(self): + button = self.wait.until( + EC.presence_of_element_located(self.BTN_MULAI_BELAJAR) + ) + + # Scroll ke elemen (cukup) + self.driver.execute_script( + "arguments[0].scrollIntoView({block: 'center'});", button + ) + + # Klik pakai JS (paling stabil untuk ) + self.driver.execute_script("arguments[0].click();", button) + + def get_km_value(self): + return self.driver.find_element(*self.KM_VALUE).text.lower() + + def get_rm_value(self): + return self.driver.find_element(*self.RM_VALUE).text.lower() + + def get_history_rows(self): + return self.driver.find_elements(*self.TABLE_ROWS) + + def click_ubah(self): + self.wait.until(EC.element_to_be_clickable(self.BTN_UBAH)).click() + + def click_unduh(self): + button = self.wait.until(EC.element_to_be_clickable(self.BTN_UNDUH)) + self.driver.execute_script("arguments[0].scrollIntoView(true);", button) + self.driver.execute_script("arguments[0].click();", button) + diff --git a/pages/kuesioner_ls_page.py b/pages/kuesioner_ls_page.py new file mode 100644 index 0000000..d8d77c3 --- /dev/null +++ b/pages/kuesioner_ls_page.py @@ -0,0 +1,85 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +class KuesionerLSPage: + + DASHBOARD_MENU = ( + By.XPATH, + "//a[contains(@href,'dashboard') or .//div[normalize-space()='Dashboard']]" + ) + + SUBMIT_BUTTON = ( + By.XPATH, "//button[normalize-space()='Simpan Jawaban']" + ) + + KUESIONER_MENU = ( + By.XPATH, + "//a[contains(@href,'kuesioner-panduan') and .//div[normalize-space()='Kuesioner']]" + ) + + MULAI_KUESIONER_BUTTON = ( + By.XPATH, + "//a[contains(@href,'/kuesioner-ls') and normalize-space()='Mulai Kuesioner']" + ) + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + def open_from_sidebar(self): + self.wait.until( + EC.element_to_be_clickable(self.KUESIONER_MENU) + ).click() + + def click_mulai_kuesioner(self): + self.wait.until( + EC.element_to_be_clickable(self.MULAI_KUESIONER_BUTTON) + ).click() + + def answer_question(self, question_number, option_index=0): + """ + option_index: 0-3 (LS punya 4 opsi) + """ + options = self.wait.until( + EC.presence_of_all_elements_located( + (By.NAME, f"soal{question_number}") + ) + ) + + option = options[option_index] + + # WAJIB scroll dulu + self.driver.execute_script( + "arguments[0].scrollIntoView({block: 'center'});", option + ) + + self.wait.until(EC.element_to_be_clickable(option)).click() + + + def answer_all_questions(self, total_questions=16): + for i in range(1, total_questions + 1): + self.answer_question(i) + + def submit(self): + button = self.wait.until( + EC.presence_of_element_located(self.SUBMIT_BUTTON) + ) + + self.driver.execute_script( + "arguments[0].scrollIntoView({block: 'center'});", button + ) + + self.wait.until(EC.element_to_be_clickable(self.SUBMIT_BUTTON)).click() + + def force_navigate_to_dashboard(self): + menu = self.wait.until( + EC.presence_of_element_located(self.DASHBOARD_MENU) + ) + + self.driver.execute_script( + "arguments[0].scrollIntoView({block: 'center'});", menu + ) + + self.wait.until(EC.element_to_be_clickable(self.DASHBOARD_MENU)).click() + diff --git a/pages/kuesioner_mai_page.py b/pages/kuesioner_mai_page.py new file mode 100644 index 0000000..9037c50 --- /dev/null +++ b/pages/kuesioner_mai_page.py @@ -0,0 +1,101 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +class KuesionerMAIPage: + + TITLE = ( + By.XPATH, + "//h3[contains(text(),'Kuesioner Metakognitif')]" + ) + + SUBMIT_BUTTON = ( + By.XPATH, + "//button[normalize-space()='Simpan Jawaban']" + ) + + KUESIONER_MENU = ( + By.XPATH, + "//a[contains(@href,'kuesioner-panduan') and .//div[normalize-space()='Kuesioner']]" + ) + + MULAI_KUESIONER_BUTTON = ( + By.XPATH, + "//a[contains(@href,'/kuesioner-ls') and normalize-space()='Mulai Kuesioner']" + ) + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 15) + + def wait_until_loaded(self): + self.wait.until( + EC.presence_of_element_located(self.TITLE) + ) + + def answer_question_mai(self, question_number, option_index=0): + """ + option_index: 0–4 (nilai 0–4) + """ + radios = self.driver.find_elements( + By.NAME, f"soal{question_number}" + ) + + target = radios[option_index] + + # WAJIB scroll agar tidak intercepted + self.driver.execute_script( + "arguments[0].scrollIntoView({block:'center'});", + target + ) + + self.wait.until(EC.element_to_be_clickable(target)).click() + + def answer_question_ls(self, question_number, option_index=0): + """ + option_index: 0-3 (LS punya 4 opsi) + """ + options = self.wait.until( + EC.presence_of_all_elements_located( + (By.NAME, f"soal{question_number}") + ) + ) + + option = options[option_index] + + # WAJIB scroll dulu + self.driver.execute_script( + "arguments[0].scrollIntoView({block: 'center'});", option + ) + + self.wait.until(EC.element_to_be_clickable(option)).click() + + def answer_all_questions_ls(self, total_questions=16): + for i in range(1, total_questions + 1): + self.answer_question_ls(i) + + def answer_all_questions_mai(self, total_questions=52): + for i in range(1, total_questions + 1): + self.answer_question_mai(i) + + def submit(self): + btn = self.wait.until( + EC.element_to_be_clickable(self.SUBMIT_BUTTON) + ) + + self.driver.execute_script( + "arguments[0].scrollIntoView({block:'center'});", + btn + ) + + btn.click() + + def open_from_sidebar(self): + self.wait.until( + EC.element_to_be_clickable(self.KUESIONER_MENU) + ).click() + + def click_mulai_kuesioner(self): + self.wait.until( + EC.element_to_be_clickable(self.MULAI_KUESIONER_BUTTON) + ).click() diff --git a/pages/kuesioner_panduan_page.py b/pages/kuesioner_panduan_page.py new file mode 100644 index 0000000..d5731e9 --- /dev/null +++ b/pages/kuesioner_panduan_page.py @@ -0,0 +1,52 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class KuesionerPanduanPage: + + URL = "https://hypermedialearning.sanggadewa.my.id/kuesioner-panduan" + + # ===== LOCATORS ===== + KUESIONER_MENU = ( + By.XPATH, + "//a[contains(@href,'kuesioner-panduan') and .//div[normalize-space()='Kuesioner']]" + ) + + MULAI_KUESIONER_BUTTON = ( + By.XPATH, + "//a[contains(@href,'/kuesioner-ls') and normalize-space()='Mulai Kuesioner']" + ) + + BANTUAN_BUTTON = ( + By.XPATH, + "//a[contains(@href,'wa.me') and contains(normalize-space(),'Bantuan')]" + ) + + # ===== INIT ===== + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 15) + + # ===== ACTIONS ===== + def open_from_sidebar(self): + self.wait.until( + EC.element_to_be_clickable(self.KUESIONER_MENU) + ).click() + + def click_mulai_kuesioner(self): + self.wait.until( + EC.element_to_be_clickable(self.MULAI_KUESIONER_BUTTON) + ).click() + + def click_bantuan(self): + self.wait.until( + EC.element_to_be_clickable(self.BANTUAN_BUTTON) + ).click() + + # ===== ASSERTIONS ===== + def is_on_panduan_page(self) -> bool: + return "kuesioner-panduan" in self.driver.current_url + + + diff --git a/pages/live_coding_page.py b/pages/live_coding_page.py new file mode 100644 index 0000000..42602f6 --- /dev/null +++ b/pages/live_coding_page.py @@ -0,0 +1,151 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import NoAlertPresentException + + +class LiveCodingPage: + + # ===== LOCATORS ===== + PAGE_TITLE = (By.XPATH, "//h4[contains(text(),'Live Coding')]") + EXPECTED_OUTPUT = (By.XPATH, "//strong[contains(text(),'Hasil Output')]") + CODE_EDITOR = (By.ID, "editor") + SUBMIT_BUTTON = (By.ID, "submit-code") + ERROR_404_TEXT = "404" + + #alert / result + RESULT_MESSAGE = (By.ID, "result") + MODAL = (By.CLASS_NAME, "modal-dialog") + CLOSE_BUTTON = (By.XPATH, "//button[normalize-space()='Tutup']") + PREVIOUS_BUTTON = (By.XPATH, "//a[contains(text(),'Sebelumnya')]") + NEXT_BUTTON = (By.XPATH, "//a[contains(text(),'Selanjutnya')]") + OTHER_MENU = (By.XPATH, "//a[contains(@href,'/materi/visual/')]") + CONFIRM_MODAL = (By.CLASS_NAME, "modal") + CONFIRM_TEXT = (By.CLASS_NAME, "modal-body") + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + # ===== PAGE ACTIONS ===== + def is_page_loaded(self): + self.wait.until(EC.visibility_of_element_located(self.PAGE_TITLE)) + return True + + def get_expected_output_text(self): + return self.driver.find_element(*self.EXPECTED_OUTPUT).text + + def editor_is_visible(self): + return self.driver.find_element(*self.CODE_EDITOR).is_displayed() + + def append_code(self, code): + """ + Menambahkan kode ke editor (tanpa menghapus kode bawaan) + """ + editor = self.driver.find_element(*self.CODE_EDITOR) + editor.click() + + actions = ActionChains(self.driver) + actions.key_down(Keys.CONTROL).send_keys(Keys.END).key_up(Keys.CONTROL) + actions.send_keys(Keys.ENTER) + actions.send_keys(code) + actions.perform() + + def append_code2(self, code): + + current_code = self.driver.execute_script(""" + return monaco.editor.getModels()[0].getValue(); + """) + + updated_code = current_code + updated_code = updated_code.replace("...............", " Mahasiswa") + updated_code = updated_code.replace(".........", ".println") + + self.driver.execute_script(""" + monaco.editor.getModels()[0].setValue(arguments[0]); + """, updated_code) + + def submit_code(self): + btn = WebDriverWait(self.driver, 15).until( + EC.element_to_be_clickable(self.SUBMIT_BUTTON) + ) + self.driver.execute_script( + "arguments[0].scrollIntoView({block:'center'});", btn + ) + self.driver.execute_script("arguments[0].click();", btn) + + def get_result_message(self): + wait = WebDriverWait(self.driver, 20) + + def result_has_text(driver): + el = driver.find_element(By.ID, "result") + return el if el.text.strip() != "" else False + + result_el = wait.until(result_has_text) + return result_el.text + + def close_result_modal(self): + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(self.CLOSE_BUTTON) + ).click() + + def get_editor_value(self): + """ + Ambil isi editor Monaco via JavaScript + """ + return self.driver.execute_script( + "return monaco.editor.getModels()[0].getValue();" + ) + + def wait_popup_visible(self): + self.wait.until(EC.visibility_of_element_located(self.RESULT_MESSAGE)) + + def is_popup_visible(self): + try: + return self.driver.find_element(*self.RESULT_MESSAGE).is_displayed() + except: + return False + + def click_previous(self): + element = self.driver.find_element(*self.PREVIOUS_BUTTON) + self.driver.execute_script("arguments[0].scrollIntoView(true);", element) + element.click() + + def click_next(self): + element = self.driver.find_element(*self.NEXT_BUTTON) + self.driver.execute_script("arguments[0].scrollIntoView(true);", element) + element.click() + + def wait_popup_invisible(self): + WebDriverWait(self.driver, 10).until( + EC.invisibility_of_element_located((By.ID, "resultModal")) + ) + + # ===== INFO ===== + def get_current_url(self): + return self.driver.current_url + + def is_404_page(self): + return self.ERROR_404_TEXT in self.driver.page_source + + def click_other_menu(self): + self.driver.find_element(*self.OTHER_MENU).click() + + # ===== ALERT HANDLER ===== + def is_confirm_alert_present(self): + try: + alert = self.driver.switch_to.alert + return alert.text + except NoAlertPresentException: + return None + + # ===== MODAL HANDLER ===== + def is_confirm_modal_visible(self): + try: + return self.driver.find_element(*self.CONFIRM_MODAL).is_displayed() + except: + return False + + diff --git a/pages/login_page.py b/pages/login_page.py new file mode 100644 index 0000000..5ab20c6 --- /dev/null +++ b/pages/login_page.py @@ -0,0 +1,169 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class LoginPage: + #URL = "https://hypermedialearning.sanggadewa.my.id/login" + URL = "https://hypermedialearning.project2025.id/public/login" + DASHBOARD_URL_PART = "/dashboard" + + # =============================== + # LOCATORS + # =============================== + EMAIL = (By.ID, "email") + PASSWORD = (By.ID, "password") + REMEMBER_ME = (By.NAME, "remember") + BUTTON_LOGIN = (By.XPATH, "//button[@type='submit']") + + ERROR_MESSAGE = (By.CLASS_NAME, "invalid-feedback") + GLOBAL_ERROR = ( + By.XPATH, + "//*[contains(text(),'These credentials do not match our records')]" + ) + + SSO_GOOGLE_BUTTON = (By.XPATH, "//a[contains(@href,'/auth/google')]") + + # =============================== + # INIT + # =============================== + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + # =============================== + # PAGE ACTIONS + # =============================== + def open(self): + self.driver.get(self.URL) + + def fill_email(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.EMAIL) + ) + field.clear() + field.send_keys(value) + + def fill_password(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.PASSWORD) + ) + field.clear() + field.send_keys(value) + + def click_login(self): + self.wait.until( + EC.element_to_be_clickable(self.BUTTON_LOGIN) + ).click() + + def click_remember_me(self): + checkbox = self.wait.until( + EC.element_to_be_clickable(self.REMEMBER_ME) + ) + if not checkbox.is_selected(): + checkbox.click() + + def click_google_sso(self): + self.wait.until( + EC.element_to_be_clickable(self.GOOGLE_SSO_BUTTON) + ).click() + + # =============================== + # BUSINESS / HELPER METHODS + # =============================== + def login(self, email: str, password: str): + self.fill_email(email) + self.fill_password(password) + self.click_login() + + def is_login_success(self) -> bool: + try: + self.wait.until( + lambda d: self.DASHBOARD_URL_PART in d.current_url + ) + return True + except: + return False + + def is_login_failed(self) -> bool: + return self.has_error_message() + + def has_error_message(self) -> bool: + return any( + el.text.strip() + for el in self.driver.find_elements(*self.ERROR_MESSAGE) + ) + + def has_global_error(self) -> bool: + try: + return self.driver.find_element( + *self.GLOBAL_ERROR + ).is_displayed() + except: + return False + + def is_field_required(self, field_name: str) -> bool: + fields = { + "email": self.EMAIL, + "password": self.PASSWORD + } + + locator = fields.get(field_name) + if not locator: + raise ValueError(f"Field '{field_name}' tidak dikenali") + + element = self.driver.find_element(*locator) + return element.get_attribute("required") is not None + + def get_email_validation_message(self): + email = self.driver.find_element(By.ID, "email") + return self.driver.execute_script( + "return arguments[0].validationMessage;", + email + ) + + def submit(self): + self.click_login() + + def has_html5_validation(self, field_name: str) -> bool: + field = self.get_field(field_name) + return field.get_attribute("validationMessage") != "" + + def get_field(self, field_name: str): + fields = { + "email": self.EMAIL, + "password": self.PASSWORD, + } + + locator = fields.get(field_name) + if not locator: + raise ValueError(f"Field '{field_name}' tidak dikenali di LoginPage") + + return self.driver.find_element(*locator) + + def click_remember_me(self): + checkbox = self.wait.until( + EC.element_to_be_clickable(self.REMEMBER_ME) + ) + if not checkbox.is_selected(): + checkbox.click() + + def is_remember_me_checked(self) -> bool: + checkbox = self.driver.find_element(*self.REMEMBER_ME) + return checkbox.is_selected() + + def click_sso_google(self): + self.wait.until( + EC.element_to_be_clickable(self.SSO_GOOGLE_BUTTON) + ).click() + + def is_redirected_to_google(self) -> bool: + self.wait.until( + lambda d: "accounts.google.com" in d.current_url + ) + return "accounts.google.com" in self.driver.current_url + + + + + diff --git a/pages/logout_page.py b/pages/logout_page.py new file mode 100644 index 0000000..29a57b3 --- /dev/null +++ b/pages/logout_page.py @@ -0,0 +1,82 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class LogoutPage: + + URL = "https://hypermedialearning.sanggadewa.my.id/dashboard" + + # --- USER ICON (pojok kanan atas) --- + USER_ICON = ( + By.XPATH, + "//li[contains(@class,'dropdown')]//a[contains(@class,'dropdown-toggle')]" + ) + + # --- MENU DROPDOWN --- + LOGOUT_MENU = (By.XPATH,"//a[@data-bs-target='#confirmlogout']") + + # --- MODAL --- + LOGOUT_MODAL = (By.ID, "confirmlogout") + MODAL_TITLE = (By.ID, "confirmlogoutLabel") + + # --- BUTTON DI MODAL --- + BUTTON_TIDAK = ( + By.XPATH, + "//div[@id='confirmlogout']//button[contains(text(),'Tidak')]" + ) + + BUTTON_YA = ( + By.XPATH, + "//div[@id='confirmlogout']//button[contains(text(),'Ya')]" + ) + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 15) + + def open(self): + self.driver.get(self.URL) + + # ============================= + # ACTIONS + # ============================= + + + def open_user_dropdown(self): + dropdown = self.wait.until( + EC.element_to_be_clickable(self.USER_ICON) + ) + dropdown.click() + + # Tunggu logout menu muncul + self.wait.until( + EC.visibility_of_element_located(self.LOGOUT_MENU) + ) + + def click_logout_menu(self): + self.wait.until( + EC.element_to_be_clickable(self.LOGOUT_MENU) + ).click() + + def is_logout_modal_visible(self): + return self.wait.until( + EC.visibility_of_element_located(self.MODAL_TITLE) + ) + + + def click_tidak(self): + self.wait.until( + EC.element_to_be_clickable(self.BUTTON_TIDAK) + ).click() + + def click_ya_logout(self): + self.wait.until( + EC.element_to_be_clickable(self.BUTTON_YA) + ).click() + + def wait_until_redirect_to_home(self): + self.wait.until( + EC.url_to_be("https://hypermedialearning.sanggadewa.my.id/") + ) + diff --git a/pages/materi_page.py b/pages/materi_page.py new file mode 100644 index 0000000..dc582b5 --- /dev/null +++ b/pages/materi_page.py @@ -0,0 +1,50 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class MateriPage: + + # ===== SIDEBAR ===== + SIDEBAR = (By.ID, "learningStyleSidebar") + + BTN_VISUAL = ( + By.XPATH, + "//div[@id='learningStyleSidebar']//h6[normalize-space()='Visual']/following-sibling::a" + ) + + BTN_AUDITORY = ( + By.XPATH, + "//div[@id='learningStyleSidebar']//h6[normalize-space()='Auditory']/following-sibling::a" + ) + + # ===== JUDUL HALAMAN ===== + TITLE_VISUAL = (By.XPATH, "//h4[normalize-space()='Materi Visual']") + TITLE_AUDITORY = (By.XPATH, "//h4[normalize-space()='Materi Auditory']") + + # ===== MEDIA ===== + VISUAL_IMAGE = (By.XPATH, "//img[@alt='Gambar Materi']") + AUDIO_PLAYER = (By.XPATH, "//audio") + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + # ===== SIDEBAR ===== + def sidebar_visible(self): + self.wait.until(EC.visibility_of_element_located(self.SIDEBAR)) + + def go_to_visual(self): + self.wait.until(EC.element_to_be_clickable(self.BTN_VISUAL)).click() + + def go_to_auditory(self): + self.wait.until(EC.element_to_be_clickable(self.BTN_AUDITORY)).click() + + # ===== ASSERTION ===== + def visual_page_loaded(self): + self.wait.until(EC.visibility_of_element_located(self.TITLE_VISUAL)) + self.wait.until(EC.visibility_of_element_located(self.VISUAL_IMAGE)) + + def auditory_page_loaded(self): + self.wait.until(EC.visibility_of_element_located(self.TITLE_AUDITORY)) + self.wait.until(EC.visibility_of_element_located(self.AUDIO_PLAYER)) diff --git a/pages/materi_pembelajaran_page.py b/pages/materi_pembelajaran_page.py new file mode 100644 index 0000000..46c13dc --- /dev/null +++ b/pages/materi_pembelajaran_page.py @@ -0,0 +1,105 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class MateriPembelajaranPage: + + # ===== SIDEBAR ===== + MENU_MATERI_PEMBELAJARAN = ( + By.XPATH, + "//div[normalize-space()='Materi Pembelajaran']/ancestor::a" + ) + + SUBMENU_ENKAPSULASI = ( + By.XPATH, + "//a[@href='/materi']//div[normalize-space()='Enkapsulasi']" + ) + + # ===== LEARNING STYLE UTAMA ===== + LEARNING_STYLE_TITLE = ( + By.XPATH, + "//h5[normalize-space()='Learning Style Kamu šŸŽ‰']" + ) + + LEARNING_STYLE_VALUE = ( + By.XPATH, + "//h5[normalize-space()='Learning Style Kamu šŸŽ‰']/following-sibling::p" + ) + + BUTTON_MULAI_BELAJAR_UTAMA = ( + By.XPATH, + "//h5[normalize-space()='Learning Style Kamu šŸŽ‰']/following::a[normalize-space()='Mulai Belajar']" + ) + + # ===== PESAN ALTERNATIF ===== + PESAN_ALTERNATIF = ( + By.XPATH, + "//h5[contains(text(),'Materi Sesuai Learning Style Kamu Sulit Dipahami')]" + ) + + # ===== CARD ALTERNATIF ===== + CARD_VISUAL = ( + By.XPATH, + "//h5[normalize-space()='Visual']/following::a[normalize-space()='Mulai Belajar'][1]" + ) + + CARD_AUDITORY = ( + By.XPATH, + "//h5[normalize-space()='Auditory']/following::a[normalize-space()='Mulai Belajar'][1]" + ) + + CARD_READ_WRITE = ( + By.XPATH, + "//h5[normalize-space()='Read/ Write']/following::a[normalize-space()='Mulai Belajar'][1]" + ) + + # ===== JUDUL HALAMAN MATERI ===== + JUDUL_HALAMAN_MATERI = ( + By.XPATH, + "//h4[@class='fw-bold mb-4']" + ) + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + # ===== ACTIONS ===== + def open_from_sidebar(self): + self.wait.until( + EC.element_to_be_clickable(self.MENU_MATERI_PEMBELAJARAN) + ).click() + + self.wait.until( + EC.element_to_be_clickable(self.SUBMENU_ENKAPSULASI) + ).click() + + def click_mulai_belajar_utama(self): + self.wait.until( + EC.element_to_be_clickable(self.BUTTON_MULAI_BELAJAR_UTAMA) + ).click() + + def click_visual(self): + self.wait.until( + EC.element_to_be_clickable(self.CARD_VISUAL) + ).click() + + def click_auditory(self): + self.wait.until( + EC.element_to_be_clickable(self.CARD_AUDITORY) + ).click() + + def click_read_write(self): + self.wait.until( + EC.element_to_be_clickable(self.CARD_READ_WRITE) + ).click() + + def get_learning_style_user(self): + return self.wait.until( + EC.visibility_of_element_located(self.LEARNING_STYLE_VALUE) + ).text + + def get_judul_materi(self): + return self.wait.until( + EC.visibility_of_element_located(self.JUDUL_HALAMAN_MATERI) + ).text diff --git a/pages/materi_readwrite_page.py b/pages/materi_readwrite_page.py new file mode 100644 index 0000000..98284ba --- /dev/null +++ b/pages/materi_readwrite_page.py @@ -0,0 +1,76 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +class MateriReadWritePage: + + # ===== Locator ===== + PAGE_TITLE = (By.XPATH, "//h4[normalize-space()='Materi Read / Write']") + TEXT_MATERI = (By.XPATH, "//strong[text()='Teks:']/following-sibling::pre") + + RANGKUMAN_TEXTAREA = (By.ID, "rangkuman") + SUBMIT_BUTTON = (By.XPATH, "//button[normalize-space()='Kirim Rangkuman']") + + SUCCESS_ALERT = (By.XPATH, "//div[contains(@class,'alert-success')]") + TUGAS_RANGKUMAN_LABEL = (By.XPATH,"//label[@for='rangkuman']//strong[normalize-space()='Tugas Rangkuman:']") + + + # Sidebar navigasi materi lain + SIDEBAR_VISUAL = ( + By.XPATH, + "//div[@id='learningStyleSidebar']//h6[normalize-space()='Visual']/following-sibling::a" + ) + SIDEBAR_AUDITORY = ( + By.XPATH, + "//div[@id='learningStyleSidebar']//h6[normalize-space()='Auditory']/following-sibling::a" + ) + SIDEBAR_KINESTHETIC = ( + By.XPATH, + "//div[@id='learningStyleSidebar']//h6[normalize-space()='Kinesthetic']/following-sibling::a" + ) + ERROR_ALERT = (By.XPATH, "//div[contains(@class,'alert-danger')]") + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + # ===== Actions ===== + def page_loaded(self): + self.wait.until(EC.visibility_of_element_located(self.PAGE_TITLE)) + + def get_text_materi(self): + return self.wait.until( + EC.visibility_of_element_located(self.TEXT_MATERI) + ).text + + def input_rangkuman(self, text): + textarea = self.wait.until( + EC.visibility_of_element_located(self.RANGKUMAN_TEXTAREA) + ) + textarea.clear() + textarea.send_keys(text) + + def submit_rangkuman(self): + self.wait.until( + EC.element_to_be_clickable(self.SUBMIT_BUTTON) + ).click() + + def success_message_displayed(self): + return self.wait.until( + EC.visibility_of_element_located(self.SUCCESS_ALERT) + ) + + # ===== Sidebar navigation ===== + def go_to_visual(self): + self.wait.until(EC.element_to_be_clickable(self.SIDEBAR_VISUAL)).click() + + def go_to_auditory(self): + self.wait.until(EC.element_to_be_clickable(self.SIDEBAR_AUDITORY)).click() + + def go_to_kinesthetic(self): + self.wait.until(EC.element_to_be_clickable(self.SIDEBAR_KINESTHETIC)).click() + + def error_message_displayed(self): + return self.wait.until( + EC.visibility_of_element_located(self.ERROR_ALERT) + ) diff --git a/pages/profile_page.py b/pages/profile_page.py new file mode 100644 index 0000000..564b135 --- /dev/null +++ b/pages/profile_page.py @@ -0,0 +1,193 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import Select +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class ProfilePage: + + # =============================== + # LOCATORS + # =============================== + + # --- Profile Display --- + EMAIL_FIELD = (By.ID, "email") + NAMA_LENGKAP_FIELD = (By.ID, "nama_lengkap") + NIM_FIELD = (By.ID, "nim") + SEMESTER_DROPDOWN = (By.ID, "semester") + ANGKATAN_DROPDOWN = (By.ID, "angkatan") + + # --- Upload Foto --- + FILE_INPUT = (By.ID, "profile_image") + PROFILE_IMAGE = (By.ID, "uploadedAvatar") + RESET_BUTTON = (By.XPATH, "//a[.//span[text()='Reset']]") + + # --- Buttons --- + SAVE_BUTTON = (By.XPATH, "//button[contains(text(),'Simpan Perubahan')]") + CANCEL_BUTTON = (By.XPATH, "//button[contains(text(),'Batal')]") + + # --- Error Messages --- + ERROR_NAMA = (By.ID, "nama_lengkap-error") + ERROR_NIM = (By.ID, "nim-error") + ERROR_SEMESTER = (By.ID, "semester-error") + ERROR_ANGKATAN = (By.ID, "angkatan-error") + + # --- Success Message --- + SUCCESS_ALERT = (By.CLASS_NAME, "alert-success") + + + # =============================== + # PROFILE DISPLAY METHODS + # =============================== + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + def get_email(self): + return self.driver.find_element(*self.EMAIL_FIELD).get_attribute("value") + + def is_email_readonly(self): + return self.driver.find_element(*self.EMAIL_FIELD).get_attribute("readonly") is not None + + def get_nama_lengkap(self): + return self.driver.find_element(*self.NAMA_LENGKAP_FIELD).get_attribute("value") + + def get_nim(self): + return self.driver.find_element(*self.NIM_FIELD).get_attribute("value") + + def get_selected_semester(self): + select = Select(self.driver.find_element(*self.SEMESTER_DROPDOWN)) + return select.first_selected_option.text + + def get_selected_angkatan(self): + select = Select(self.driver.find_element(*self.ANGKATAN_DROPDOWN)) + return select.first_selected_option.text + + def get_all_semester_options(self): + """ + Mengambil seluruh opsi semester dari dropdown + Return: list of string (contoh: ["1","2","3","4","5","6","7","8"]) + """ + select_element = Select(self.driver.find_element(*self.SEMESTER_DROPDOWN)) + options = select_element.options + + return [option.text.strip() for option in options] + + def get_all_angkatan_options(self): + """ + Mengambil seluruh opsi angkatan dari dropdown + Return: list of string (contoh: ["2020","2021","3","4","5","6","7","8"]) + """ + select_element = Select(self.driver.find_element(*self.ANGKATAN_DROPDOWN)) + options = select_element.options + + return [option.text.strip() for option in options] + + + # =============================== + # EDIT METHODS + # =============================== + + def set_nama_lengkap(self, name): + field = self.driver.find_element(*self.NAMA_LENGKAP_FIELD) + field.clear() + field.send_keys(name) + + def set_nim(self, nim): + field = self.driver.find_element(*self.NIM_FIELD) + field.clear() + field.send_keys(nim) + + def select_semester(self, value): + Select(self.driver.find_element(*self.SEMESTER_DROPDOWN)).select_by_value(value) + + def select_angkatan(self, value): + Select(self.driver.find_element(*self.ANGKATAN_DROPDOWN)).select_by_value(value) + + # =============================== + # UPLOAD METHODS + # =============================== + + def upload_photo(self, file_path): + self.driver.find_element(*self.FILE_INPUT).send_keys(file_path) + + def get_profile_image_src(self): + return self.driver.find_element(*self.PROFILE_IMAGE).get_attribute("src") + + def click_reset_photo(self): + wait = WebDriverWait(self.driver, 10) + + reset_btn = wait.until( + EC.element_to_be_clickable(self.RESET_BUTTON) + ) + + self.driver.execute_script("arguments[0].scrollIntoView(true);", reset_btn) + self.driver.execute_script("arguments[0].click();", reset_btn) + + + # =============================== + # BUTTON ACTIONS + # =============================== + + def click_save(self): + wait = WebDriverWait(self.driver, 10) + + save_btn = wait.until( + EC.element_to_be_clickable(self.SAVE_BUTTON) + ) + + # scroll dulu supaya tidak ketutup elemen + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", save_btn) + + wait.until(EC.element_to_be_clickable(self.SAVE_BUTTON)) + save_btn.click() + + def click_cancel(self): + cancel_btn = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(self.CANCEL_BUTTON) + ) + self.driver.execute_script("arguments[0].scrollIntoView(true);", cancel_btn) + cancel_btn.click() + + + # =============================== + # UTILITIES + # =============================== + + def refresh_page(self): + self.driver.refresh() + + def wait_until_page_loaded(self): + self.wait.until( + EC.presence_of_element_located(self.EMAIL_FIELD) + ) + + # =============================== + # VALIDATION MESSAGE METHODS + # =============================== + + def get_error_nama(self): + return self.wait.until( + EC.visibility_of_element_located(self.ERROR_NAMA) + ).text + + def get_error_nim(self): + return self.wait.until( + EC.visibility_of_element_located(self.ERROR_NIM) + ).text + + def get_error_semester(self): + return self.wait.until( + EC.visibility_of_element_located(self.ERROR_SEMESTER) + ).text + + def get_error_angkatan(self): + return self.wait.until( + EC.visibility_of_element_located(self.ERROR_ANGKATAN) + ).text + + def wait_until_reload_after_save(self): + self.wait.until( + EC.presence_of_element_located(self.EMAIL_FIELD) + ) diff --git a/pages/register_page.py b/pages/register_page.py new file mode 100644 index 0000000..1bddfad --- /dev/null +++ b/pages/register_page.py @@ -0,0 +1,202 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class RegisterPage: + #URL = "https://hypermedialearning.sanggadewa.my.id/register" + URL = "https://hypermedialearning.project2025.id/public/register" + # =============================== + # LOCATORS + # =============================== + NAMA_LENGKAP = (By.ID, "nama_lengkap") + NIM = (By.ID, "nim") + SEMESTER = (By.ID, "semester") + ANGKATAN = (By.ID, "angkatan") + EMAIL = (By.ID, "email") + PASSWORD = (By.NAME, "password") + KONFIRMASI_PASSWORD = (By.NAME, "password_confirmation") + BUTTON_DAFTAR = (By.XPATH, "//button[@type='submit']") + + ERROR_MESSAGE = (By.CLASS_NAME, "invalid-feedback") + EMAIL_ERROR_MESSAGE = ( + By.XPATH, + "//*[contains(text(),'Email harus menggunakan domain')]" + ) + + # =============================== + # INIT + # =============================== + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + # =============================== + # PAGE ACTIONS + # =============================== + def open(self): + self.driver.get(self.URL) + + def fill_nama_lengkap(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.NAMA_LENGKAP) + ) + field.clear() + field.send_keys(value) + + def fill_nim(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.NIM) + ) + field.clear() + field.send_keys(value) + + def fill_semester(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.SEMESTER) + ) + field.clear() + field.send_keys(value) + + def fill_angkatan(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.ANGKATAN) + ) + field.clear() + field.send_keys(value) + + def fill_email(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.EMAIL) + ) + field.clear() + field.send_keys(value) + + def fill_password(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.PASSWORD) + ) + field.clear() + field.send_keys(value) + + def fill_confirm_password(self, value: str): + field = self.wait.until( + EC.visibility_of_element_located(self.KONFIRMASI_PASSWORD) + ) + field.clear() + field.send_keys(value) + + def click_daftar(self): + self.wait.until( + EC.element_to_be_clickable(self.BUTTON_DAFTAR) + ).click() + + # =============================== + # HELPER / BUSINESS METHODS + # =============================== + def fill_form(self, data: dict): + self.fill_nama_lengkap(data["nama_lengkap"]) + self.fill_nim(data["nim"]) + self.fill_semester(data["semester"]) + self.fill_angkatan(data["angkatan"]) + self.fill_email(data["email"]) + self.fill_password(data["password"]) + + if "konfirmasi_password" in data: + self.fill_confirm_password(data["konfirmasi_password"]) + + pwd = self.driver.find_element(*self.PASSWORD).get_attribute("value") + cpwd = self.driver.find_element(*self.KONFIRMASI_PASSWORD).get_attribute("value") + + print("PASSWORD FIELD :", repr(pwd)) + print("CONFIRM FIELD :", repr(cpwd)) + + + + def submit(self): + """Alias agar test lebih readable""" + self.click_daftar() + + def get_error_messages(self): + elements = self.driver.find_elements(*self.ERROR_MESSAGE) + return [el.text for el in elements if el.text.strip()] + + def has_error(self, field_name: str) -> bool: + """ + Mengecek apakah error message terkait field tertentu muncul + """ + return any( + field_name.lower() in err.lower() + for err in self.get_error_messages() + ) + + def is_email_domain_error_displayed(self) -> bool: + try: + return self.driver.find_element( + *self.EMAIL_ERROR_MESSAGE + ).is_displayed() + except Exception: + return False + + def is_register_success(self) -> bool: + """ + Register dianggap sukses jika terjadi redirect dari halaman register. + """ + self.wait.until( + lambda d: d.current_url != self.URL + ) + return True + + def is_field_required(self, label_text: str) -> bool: + """ + Mengecek apakah field dengan label tertentu memiliki atribut required + (HTML5 native validation) + """ + fields = { + "nama_lengkap": self.NAMA_LENGKAP, + "NIM": self.NIM, + "nim": self.NIM, + "semester": self.SEMESTER, + "angkatan": self.ANGKATAN, + "email": self.EMAIL, + "password": self.PASSWORD, + "konfirmasi_password":self.KONFIRMASI_PASSWORD, + } + + locator = fields.get(label_text) + if not locator: + raise ValueError(f"Field '{label_text}' tidak dikenali") + + element = self.driver.find_element(*locator) + return element.get_attribute("required") is not None + + def get_value(self, field_name: str) -> str: + field = self.get_field(field_name) + return field.get_attribute("value") + + def get_field(self, field_name: str): + fields = { + "nama_lengkap": self.NAMA_LENGKAP, + "NIM": self.NIM, + "nim": self.NIM, + "semester": self.SEMESTER, + "Semester": self.SEMESTER, + "angkatan": self.ANGKATAN, + "Angkatan": self.ANGKATAN, + "email": self.EMAIL, + "password": self.PASSWORD, + "konfirmasi_password":self.KONFIRMASI_PASSWORD, + } + + locator = fields.get(field_name) + if not locator: + raise ValueError(f"Field '{field_name}' tidak dikenali") + + return self.driver.find_element(*locator) + + def has_html5_validation(self, field_name: str) -> bool: + field = self.get_field(field_name) + return field.get_attribute("validationMessage") != "" + + + diff --git a/pages/resume_pembelajaran_page.py b/pages/resume_pembelajaran_page.py new file mode 100644 index 0000000..162dc81 --- /dev/null +++ b/pages/resume_pembelajaran_page.py @@ -0,0 +1,45 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException + +class ResumePembelajaranPage: + + # ===== Locator ===== + PAGE_TITLE = (By.XPATH, "//h4[normalize-space()='Resume Pembelajaran']") + + EMPTY_MESSAGE = ( + By.CSS_SELECTOR, + "div.text-center.text-muted" +) + + RESUME_CARD = (By.CSS_SELECTOR, "div.card") + RESUME_CONTENT = (By.CSS_SELECTOR, "div.card-body pre") + + def __init__(self, driver): + self.driver = driver + self.wait = WebDriverWait(driver, 10) + + # ===== Assertions / Getters ===== + def page_loaded(self): + self.wait.until(EC.visibility_of_element_located(self.PAGE_TITLE)) + + + def is_empty_message_displayed(self): + try: + self.wait.until( + EC.presence_of_element_located(self.EMPTY_MESSAGE) + ) + return True + except TimeoutException: + return False + + def is_resume_displayed(self): + return self.wait.until( + EC.visibility_of_element_located(self.RESUME_CARD) + ) + + def get_resume_text(self): + return self.wait.until( + EC.visibility_of_element_located(self.RESUME_CONTENT) + ).text diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..156b2be --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = -v -s +testpaths = tests +markers = dashboard: test untuk fitur dashboard \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..61d12f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +selenium +pytest +webdriver-manager +.\venv\Scripts\Activate.ps1 (perintah aktifkan venv) + +def fill_email(self, email): + email_field = self.driver.find_element(*self.EMAIL) + email_field.clear() + email_field.send_keys(email) + + + + def fill_required_fields_except_email(self): + self.fill_nama("Mahasiswa Polije") + self.fill_password("Password123!") + self.fill_konfirmasi_password("Password123!") \ No newline at end of file diff --git a/tests/dashboard/test_dashboard_pengisian_kuesioner.py b/tests/dashboard/test_dashboard_pengisian_kuesioner.py new file mode 100644 index 0000000..4b1dffe --- /dev/null +++ b/tests/dashboard/test_dashboard_pengisian_kuesioner.py @@ -0,0 +1,25 @@ +import pytest +from pages.dashboard_page import DashboardPage + +@pytest.mark.dashboard +class TestDashboardPengisianKuesioner: + + def test_section_pengisian_kuesioner_tampil(self, driver, login_as_user_sudah_kuesioner): + dashboard = DashboardPage(driver) + dashboard.open() + + assert dashboard.is_pengisian_kuesioner_section_visible() + + def test_kuesioner_vark_mai_tampil(self, driver, login_as_user_sudah_kuesioner): + dashboard = DashboardPage(driver) + dashboard.open() + + assert dashboard.is_kuesioner_vark_mai_visible() + + def test_click_isi_kuesioner_redirect(self, driver, login_as_user_sudah_kuesioner): + dashboard = DashboardPage(driver) + dashboard.open() + + dashboard.click_isi_kuesioner() + + assert "kuesioner" in driver.current_url diff --git a/tests/dashboard/test_dashboard_popup.py b/tests/dashboard/test_dashboard_popup.py new file mode 100644 index 0000000..5d85348 --- /dev/null +++ b/tests/dashboard/test_dashboard_popup.py @@ -0,0 +1,30 @@ +import pytest +from pages.dashboard_page import DashboardPage + + +@pytest.mark.dashboard +class TestDashboardPopup: + + def test_popup_muncul_jika_belum_isi_kuesioner(self, driver, login_as_user_belum_kuesioner2): + dashboard = DashboardPage(driver) + dashboard.open() + + assert dashboard.is_popup_visible() + + def test_button_isi_kuesioner_redirect(self, + driver, login_as_user_belum_kuesionerNew2): + dashboard = DashboardPage(driver) + dashboard.open() + + dashboard.click_isi_kuesioner_pop_up() + + assert dashboard.is_redirected_to_kuesioner() + + def test_button_close_x_menutup_popup(self, driver, login_as_user_belum_kuesioner2): + dashboard = DashboardPage(driver) + dashboard.open() + + dashboard.close_popup_with_x() + dashboard.wait_popup_disappear() + + assert not dashboard.is_popup_visible() diff --git a/tests/dashboard/test_dashboard_status_km_rm.py b/tests/dashboard/test_dashboard_status_km_rm.py new file mode 100644 index 0000000..b7325a9 --- /dev/null +++ b/tests/dashboard/test_dashboard_status_km_rm.py @@ -0,0 +1,48 @@ +import pytest +from pages.dashboard_page import DashboardPage + + +@pytest.mark.dashboard +class TestDashboardStatusKMRM: + + def test_status_km_belum_diisi(self, driver, login_as_user_belum_kuesioner2): + dashboard = DashboardPage(driver) + dashboard.open() + dashboard.close_kuesioner_popup_if_present() + print("KM TEXT =", dashboard.get_km_status_text()) + assert dashboard.is_km_not_filled() + + def test_status_rm_belum_diisi(self, driver, login_as_user_belum_kuesioner2): + dashboard = DashboardPage(driver) + dashboard.open() + dashboard.close_kuesioner_popup_if_present() + print("RM TEXT =", dashboard.get_rm_status_text()) + assert dashboard.is_rm_not_filled() + + def test_status_km_sudah_isi_kuesioner(self, driver, login_as_user_sudah_kuesioner): + dashboard = DashboardPage(driver) + assert dashboard.is_km_filled() + print("KM TEXT =", dashboard.get_km_status_text()) + + def test_status_rm_sudah_isi_kuesioner(self, driver, login_as_user_sudah_kuesioner): + dashboard = DashboardPage(driver) + assert dashboard.is_rm_filled() + print("RM TEXT =", dashboard.get_rm_status_text()) + + def test_learning_style_belum_diisi(self, driver, login_as_user_belum_kuesioner2): + dashboard = DashboardPage(driver) + dashboard.open() + dashboard.close_kuesioner_popup_if_present() + assert dashboard.is_learning_style_not_filled() + print("LEARNING STYLE =", dashboard.get_learning_style_text()) + + def test_learning_style_sudah_diisi(self, + driver, login_as_user_sudah_kuesioner3): + dashboard = DashboardPage(driver) + dashboard.open() + assert dashboard.is_learning_style_filled() + print("LEARNING STYLE =", dashboard.get_learning_style_text()) + + + + \ No newline at end of file diff --git a/tests/history/test_history_kuesioner.py b/tests/history/test_history_kuesioner.py new file mode 100644 index 0000000..e53d480 --- /dev/null +++ b/tests/history/test_history_kuesioner.py @@ -0,0 +1,107 @@ +import re +import pytest +from pages.history_kuesioner_page import HistoryKuesionerPage +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +@pytest.mark.usefixtures("login_as_user_belum_kuesioner") +class TestHistoryKuesioner: + + def test_history_page_loaded(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + assert page.is_page_loaded() + + def test_km_dan_rm_value(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + + assert page.get_km_value() in ["low", "medium", "high"] + assert page.get_rm_value() in ["low", "medium", "high"] + + def test_ringkasan_skor_gaya_belajar(self, driver): + """ + PASSED berarti: + - 4 gaya belajar muncul + - Label sesuai requirement + - Nilai skor valid (angka) + """ + page = HistoryKuesionerPage(driver) + page.open() + scores = page.get_scores() + + expected_labels = {"Visual", "Auditory", "Read/Write", "Kinesthetic"} + + assert set(scores.keys()) == expected_labels, ( + f"Label tidak sesuai: {scores.keys()}" + ) + + for label, value in scores.items(): + assert value.isdigit(), f"Skor {label} bukan angka: {value}" + + def test_progress_pengisian_100_persen(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + + progress = page.get_progress_value() + assert progress == "100", f"Progress seharusnya 100%, tetapi {progress}%" + + def test_gaya_belajar_dominan_valid(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + + dominant = page.get_dominant_style() + assert dominant in ["Visual", "Auditory", "Read/Write", "Kinesthetic"], ( + f"Gaya dominan tidak valid: {dominant}" + ) + + def test_button_mulai_belajar_redirect(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + + page.click_mulai() + + WebDriverWait(driver, 10).until( + lambda d: "materi" in d.current_url + ) + + assert "materi" in driver.current_url + + + def test_tabel_riwayat_memuat_data(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + + rows = page.get_history_rows() + assert len(rows) > 0, "Tabel riwayat kosong" + + def test_format_tanggal_valid(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + + row_text = page.get_history_rows()[0].text + assert re.search(r"\d{2}\s[A-Za-z]{3}\s\d{4}", row_text), ( + f"Format tanggal tidak valid: {row_text}" + ) + + def test_button_ubah_redirect(self, driver): + page = HistoryKuesionerPage(driver) + page.open() + + page.click_ubah() + WebDriverWait(driver, 5).until(EC.url_contains("kuesioner")) + assert "kuesioner" in driver.current_url + + def test_button_unduh_redirect(self, driver): + """ + PASSED berarti: + - Tombol bisa diklik + - Redirect ke halaman user-result berhasil + """ + page = HistoryKuesionerPage(driver) + page.open() + + page.click_unduh() + + WebDriverWait(driver, 5).until(EC.url_contains("user-result")) + assert "user-result" in driver.current_url \ No newline at end of file diff --git a/tests/kuesioner/test_kuesioner_ls.py b/tests/kuesioner/test_kuesioner_ls.py new file mode 100644 index 0000000..013e3b0 --- /dev/null +++ b/tests/kuesioner/test_kuesioner_ls.py @@ -0,0 +1,50 @@ +import pytest +from pages.kuesioner_ls_page import KuesionerLSPage + +class TestKuesionerLS: + + def test_open_kuesioner_ls(self, driver, login_as_user_belum_kuesioner): + page = KuesionerLSPage(driver) + page.open_from_sidebar() + page.click_mulai_kuesioner() + + assert "kuesioner-ls" in driver.current_url + + def test_submit_without_answer_should_fail(self, driver, login_as_user_belum_kuesioner): + page = KuesionerLSPage(driver) + page.open_from_sidebar() + page.click_mulai_kuesioner() + + # jawab hanya 15 dari 16 + for i in range(1, 16): + page.answer_question(i) + + page.submit() + + assert "kuesioner-ls" in driver.current_url + + def test_submit_success_redirect_to_mai(self, driver, login_as_user_belum_kuesioner): + page = KuesionerLSPage(driver) + page.open_from_sidebar() + page.click_mulai_kuesioner() + + page.answer_all_questions() + page.submit() + + assert "kuesioner-mai" in driver.current_url + + def test_force_navigation_before_submit_allowed_known_issue( + self, driver, login_as_user_belum_kuesioner + ): + page = KuesionerLSPage(driver) + page.open_from_sidebar() + page.click_mulai_kuesioner() + + # jawab sebagian (belum selesai) + page.answer_question(1) + page.answer_question(2) + + # user paksa pindah halaman + page.force_navigate_to_dashboard() + + assert "kuesioner" in driver.current_url diff --git a/tests/kuesioner/test_kuesioner_mai.py b/tests/kuesioner/test_kuesioner_mai.py new file mode 100644 index 0000000..3556cb7 --- /dev/null +++ b/tests/kuesioner/test_kuesioner_mai.py @@ -0,0 +1,40 @@ +import pytest +from pages.kuesioner_mai_page import KuesionerMAIPage + +class TestKuesionerMAI: + + def test_open_kuesioner_mai(self, driver, login_as_user_belum_kuesioner): + page = KuesionerMAIPage(driver) + page.open_from_sidebar() + page.click_mulai_kuesioner() + page.answer_all_questions_ls() + page.submit() + + assert "kuesioner-mai" in driver.current_url + + def test_submit_mai_without_answer_should_fail(self, driver, login_as_user_belum_kuesioner): + page = KuesionerMAIPage(driver) + page.open_from_sidebar() + page.click_mulai_kuesioner() + page.answer_all_questions_ls() + page.submit() + + # jawab hanya 50 dari 52 + for i in range(2, 52): + page.answer_question_mai(i) + + page.submit() + + # tetap di halaman MAI + assert "kuesioner-mai" in driver.current_url + + def test_submit_mai_success_redirect_to_history(self, driver, login_as_user_belum_kuesioner): + page = KuesionerMAIPage(driver) + page.open_from_sidebar() + page.click_mulai_kuesioner() + page.answer_all_questions_ls() + page.submit() + page.answer_all_questions_mai() + page.submit() + + assert "history_quis" in driver.current_url diff --git a/tests/kuesioner/test_kuesioner_panduan.py b/tests/kuesioner/test_kuesioner_panduan.py new file mode 100644 index 0000000..f442de3 --- /dev/null +++ b/tests/kuesioner/test_kuesioner_panduan.py @@ -0,0 +1,30 @@ +import pytest +from pages.kuesioner_panduan_page import KuesionerPanduanPage + + +class TestKuesionerPanduan: + + def test_open_panduan_from_sidebar(self, + driver, login_as_user_sudah_kuesioner): + panduan = KuesionerPanduanPage(driver) + panduan.open_from_sidebar() + + assert panduan.is_on_panduan_page() + + def test_click_mulai_kuesioner_redirect(self, driver, login_as_user_sudah_kuesioner): + panduan = KuesionerPanduanPage(driver) + panduan.open_from_sidebar() + panduan.click_mulai_kuesioner() + + assert "kuesioner-ls" in driver.current_url + + def test_click_bantuan_redirect_whatsapp(self, driver, login_as_user_sudah_kuesioner): + panduan = KuesionerPanduanPage(driver) + panduan.open_from_sidebar() + panduan.click_bantuan() + + current_url = driver.current_url + + assert "whatsapp.com" in current_url + assert "6285290543351" in current_url + diff --git a/tests/login/test_login_email.py b/tests/login/test_login_email.py new file mode 100644 index 0000000..a2bc23a --- /dev/null +++ b/tests/login/test_login_email.py @@ -0,0 +1,72 @@ +import pytest + + +@pytest.mark.usefixtures("driver") +class TestLoginEmail: + """ + TEST LOGIN - EMAIL FIELD + """ + + VALID_EMAIL = "e41222052@student.polije.ac.id" + VALID_PASSWORD = "e41222052@student.polije.ac.id" + + # ========================= + # POSITIVE TEST + # ========================= + + def test_login_email_valid(self, login_page): + login_page.open() + login_page.login( + self.VALID_EMAIL, + self.VALID_PASSWORD + ) + + assert login_page.is_login_success() + + # ========================= + # NEGATIVE TESTS + # ========================= + + def test_login_email_salah_password_benar(self, login_page): + login_page.open() + login_page.login( + "salah@student.polije.ac.id", + self.VALID_PASSWORD + ) + + assert login_page.has_global_error() + + def test_login_email_kosong(self, login_page): + login_page.open() + login_page.fill_email("") + login_page.fill_password(self.VALID_PASSWORD) + login_page.click_login() + + assert login_page.is_field_required("email") + + def test_login_email_tanpa_at(self, login_page): + login_page.open() + login_page.login( + "e41222052student.polije.ac.id", + self.VALID_PASSWORD + ) + + assert login_page.get_email_validation_message() + + def test_login_email_hanya_spasi(self, login_page): + login_page.open() + login_page.login( + " ", + self.VALID_PASSWORD + ) + + assert login_page.get_email_validation_message() + + def test_login_email_tidak_terdaftar(self, login_page): + login_page.open() + login_page.login( + "tidakterdaftar@student.polije.ac.id", + self.VALID_PASSWORD + ) + + assert login_page.has_global_error() diff --git a/tests/login/test_login_password.py b/tests/login/test_login_password.py new file mode 100644 index 0000000..412d489 --- /dev/null +++ b/tests/login/test_login_password.py @@ -0,0 +1,50 @@ +import pytest + + +class TestLoginPassword: + + VALID_EMAIL = "e41222052@student.polije.ac.id" + VALID_PASSWORD = "e41222052@student.polije.ac.id" + + # ========================== + # PASSWORD NEGATIVE CASES + # ========================== + + def test_password_kosong(self, login_page): + login_page.fill_email(self.VALID_EMAIL) + login_page.fill_password("") + login_page.submit() + + assert login_page.has_html5_validation("password") + + def test_password_salah(self, login_page): + login_page.fill_email(self.VALID_EMAIL) + login_page.fill_password("PasswordSalah123!") + login_page.submit() + + assert login_page.is_login_failed() + + def test_password_hanya_spasi(self, login_page): + login_page.fill_email(self.VALID_EMAIL) + login_page.fill_password(" ") + login_page.submit() + + assert login_page.is_login_failed() + + def test_password_kurang_dari_8_karakter(self, login_page): + login_page.fill_email(self.VALID_EMAIL) + login_page.fill_password("Abc1!") + login_page.submit() + + assert login_page.is_login_failed() + + # ========================== + # PASSWORD POSITIVE CASE + # ========================== + + def test_password_valid(self, login_page): + login_page.fill_email(self.VALID_EMAIL) + login_page.fill_password(self.VALID_PASSWORD) + login_page.submit() + + assert login_page.is_login_success() diff --git a/tests/login/test_login_remember_me.py b/tests/login/test_login_remember_me.py new file mode 100644 index 0000000..c5bc5fb --- /dev/null +++ b/tests/login/test_login_remember_me.py @@ -0,0 +1,28 @@ +import pytest + +@pytest.mark.usefixtures("driver") +class TestLoginRememberMe: + + VALID_EMAIL = "e41222052@student.polije.ac.id" + VALID_PASSWORD = "e41222052@student.polije.ac.id" + + def test_remember_me_checkbox_dapat_dicentang(self, login_page): + login_page.fill_email(self.VALID_EMAIL) + login_page.fill_password(self.VALID_PASSWORD) + login_page.click_remember_me() + + assert login_page.is_remember_me_checked() + + def test_login_dengan_remember_me_dan_refresh(self, login_page, driver): + login_page.fill_email(self.VALID_EMAIL) + login_page.fill_password(self.VALID_PASSWORD) + login_page.click_remember_me() + login_page.submit() + + assert login_page.is_login_success() + + # refresh halaman + driver.refresh() + + # masih di dashboard + assert login_page.is_login_success() diff --git a/tests/login/test_login_sso.py b/tests/login/test_login_sso.py new file mode 100644 index 0000000..a3f42d4 --- /dev/null +++ b/tests/login/test_login_sso.py @@ -0,0 +1,13 @@ +import pytest + +class TestLoginSSOGoogle: + + def test_button_sso_google_tersedia(self, login_page): + assert login_page.driver.find_element( + *login_page.SSO_GOOGLE_BUTTON + ) + + def test_redirect_ke_google_oauth(self, login_page): + login_page.click_sso_google() + + assert login_page.is_redirected_to_google() diff --git a/tests/logout/test_logout.py b/tests/logout/test_logout.py new file mode 100644 index 0000000..1547363 --- /dev/null +++ b/tests/logout/test_logout.py @@ -0,0 +1,46 @@ +import pytest +from pages.logout_page import LogoutPage + + +class TestLogout: + + # =============================== + # 1ļøāƒ£ Test: Modal Muncul + # =============================== + def test_logout_modal_muncul(self, driver, login_as_user_sudah_kuesioner): + logout = LogoutPage(driver) + + logout.open_user_dropdown() + logout.click_logout_menu() + + assert logout.is_logout_modal_visible() + + + # =============================== + # 2ļøāƒ£ Test: Klik Tidak → Tetap di halaman + # =============================== + def test_logout_tidak_tetap_di_dashboard(self, driver, login_as_user_sudah_kuesioner): + logout = LogoutPage(driver) + + current_url = driver.current_url + + logout.open_user_dropdown() + logout.click_logout_menu() + logout.click_tidak() + + assert driver.current_url == current_url + + + # =============================== + # 3ļøāƒ£ Test: Klik Ya → Redirect ke Home + # =============================== + def test_logout_ya_redirect_ke_home(self, driver, login_as_user_sudah_kuesioner): + logout = LogoutPage(driver) + + logout.open_user_dropdown() + logout.click_logout_menu() + logout.click_ya_logout() + + logout.wait_until_redirect_to_home() + + assert "dashboard" not in driver.current_url.lower() diff --git a/tests/materi/test_materi_auditory.py b/tests/materi/test_materi_auditory.py new file mode 100644 index 0000000..025e64e --- /dev/null +++ b/tests/materi/test_materi_auditory.py @@ -0,0 +1,46 @@ +from pages.materi_page import MateriPage + + +class TestMateriAuditory: + + def test_open_auditory_material(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/auditory/1") + + page = MateriPage(driver) + page.auditory_page_loaded() + page.sidebar_visible() + + assert "materi/auditory" in driver.current_url + + def test_auditory_has_audio_player(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/auditory/1") + + page = MateriPage(driver) + page.auditory_page_loaded() + + audio = page.driver.find_element(*page.AUDIO_PLAYER) + assert audio.is_displayed() + assert audio.get_attribute("controls") is not None + + def test_navigate_auditory_to_visual(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/auditory/1") + + page = MateriPage(driver) + page.go_to_visual() + page.visual_page_loaded() + + assert "materi/visual" in driver.current_url + + def test_navigation_requires_confirmation_from_auditory( + self, driver, login_as_user_sudah_kuesioner + ): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/auditory/1") + + page = MateriPage(driver) + + # User mencoba pindah sebelum selesai belajar + page.go_to_visual() + + # Expected behavior (ideal requirement): + assert "materi/auditory" in driver.current_url, \ + "User seharusnya tidak bisa meninggalkan halaman sebelum selesai belajar" diff --git a/tests/materi/test_materi_kinesthetic.py b/tests/materi/test_materi_kinesthetic.py new file mode 100644 index 0000000..3139f47 --- /dev/null +++ b/tests/materi/test_materi_kinesthetic.py @@ -0,0 +1,141 @@ +import pytest +from pages.live_coding_page import LiveCodingPage + +class TestKinesthetic: + + def test_live_coding_page_loaded(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + + assert page.is_page_loaded() + assert page.editor_is_visible() + assert "Hasil Output" in page.get_expected_output_text() + + def test_submit_without_completing_code(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + + page.submit_code() + result = page.get_result_message() + assert "Kompilasi Gagal" in result + + """assert "Jawaban Salah" in result + assert "Seharusnya" in result""" + + def test_submit_with_wrong_code(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + + page.append_code("System.out.println(\"SALAH\");") + page.submit_code() + + result = page.get_result_message() + assert "Kompilasi Gagal" in result + + """assert "Jawaban Salah" in result + assert "Seharusnya" in result""" + + def test_submit_with_correct_code(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + + page.append_code( + "System.out.println(m1.getNama() + \" | IPK: \" + m1.getIpk() + \"\\n\" + m2.getNama() + \" | IPK: \" + m2.getIpk());") + page.submit_code() + + result = page.get_result_message() + assert "Jawaban Benar" in result + + def test_default_code_is_displayed(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + code = page.get_editor_value() + + assert code.strip() != "", \ + "FAIL: Kode bawaan materi tidak tampil pada editor" + + def test_popup_can_be_closed(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + page.append_code("\nprint('test')") + page.submit_code() + + page.wait_popup_visible() + assert page.is_popup_visible() + + page.close_result_modal() + page.wait_popup_invisible() + + assert not page.is_popup_visible(), \ + "FAIL: Popup evaluasi tidak tertutup" + + def test_previous_button_on_first_page(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + initial_url = page.get_current_url() + + page.click_previous() + + current_url = page.get_current_url() + + assert current_url == initial_url, ( + "FAIL: Tombol Sebelumnya pada halaman pertama " + "seharusnya tidak berpindah halaman" + ) + + assert not page.is_404_page(), ( + "FAIL: Tombol Sebelumnya menyebabkan halaman 404" + ) + + def test_next_button_on_first_page(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + initial_url = page.get_current_url() + + page.click_next() + + current_url = page.get_current_url() + + assert current_url != initial_url, ( + "FAIL: Tombol Selanjutnya tidak berpindah ke halaman berikutnya" + ) + + assert not page.is_404_page(), ( + "FAIL: Tombol Selanjutnya menyebabkan halaman 404" + ) + + def test_navigation_without_submit_shows_validation(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/kinesthetic/1") + page = LiveCodingPage(driver) + """ + User berpindah halaman sebelum submit kode + Expected: muncul notifikasi validasi + """ + + initial_url = page.get_current_url() + page.append_code("\nSystem.out.println('Belum submit');") + page.click_other_menu() + + # ===== CASE 1: JS ALERT ===== + alert_text = page.is_confirm_alert_present() + if alert_text: + assert ( + "yakin" in alert_text.lower() + or "belum" in alert_text.lower() + ), "FAIL: Teks alert tidak sesuai validasi" + return + + # ===== CASE 2: HTML MODAL ===== + assert page.is_confirm_modal_visible(), ( + "FAIL: Tidak ada alert atau modal validasi saat berpindah halaman" + ) + + # Pastikan belum pindah halaman + assert page.get_current_url() == initial_url, ( + "FAIL: Sistem berpindah halaman tanpa konfirmasi" + ) + + + + + diff --git a/tests/materi/test_materi_pembelajaran_enkapsulasi.py b/tests/materi/test_materi_pembelajaran_enkapsulasi.py new file mode 100644 index 0000000..67989fa --- /dev/null +++ b/tests/materi/test_materi_pembelajaran_enkapsulasi.py @@ -0,0 +1,55 @@ +import pytest +from pages.materi_pembelajaran_page import MateriPembelajaranPage + + +class TestMateriPembelajaranEnkapsulasi: + + def test_open_halaman_enkapsulasi(self, driver, login_as_user_sudah_kuesioner): + page = MateriPembelajaranPage(driver) + page.open_from_sidebar() + + assert "/materi" in driver.current_url + + def test_tampilkan_learning_style_user(self, driver, login_as_user_sudah_kuesioner): + page = MateriPembelajaranPage(driver) + page.open_from_sidebar() + + style = page.get_learning_style_user() + assert style in ["Visual", "Auditory", "Read/Write", "Kinesthetic"] + + def test_mulai_belajar_learning_style_utama(self, driver, login_as_user_sudah_kuesioner): + page = MateriPembelajaranPage(driver) + page.open_from_sidebar() + page.click_mulai_belajar_utama() + + judul = page.get_judul_materi() + assert judul != "" + + def test_pesan_materi_alternatif_muncul(self, driver, login_as_user_sudah_kuesioner): + page = MateriPembelajaranPage(driver) + page.open_from_sidebar() + + assert page.wait.until( + lambda d: d.find_element(*page.PESAN_ALTERNATIF) + ) + + def test_mulai_belajar_visual(self, driver, login_as_user_sudah_kuesioner): + page = MateriPembelajaranPage(driver) + page.open_from_sidebar() + page.click_visual() + + assert page.get_judul_materi() == "Materi Visual" + + def test_mulai_belajar_auditory(self, driver, login_as_user_sudah_kuesioner): + page = MateriPembelajaranPage(driver) + page.open_from_sidebar() + page.click_auditory() + + assert page.get_judul_materi() == "Materi Auditory" + + def test_mulai_belajar_read_write(self, driver, login_as_user_sudah_kuesioner): + page = MateriPembelajaranPage(driver) + page.open_from_sidebar() + page.click_read_write() + + assert page.get_judul_materi() == "Materi Read / Write" diff --git a/tests/materi/test_materi_readwrite.py b/tests/materi/test_materi_readwrite.py new file mode 100644 index 0000000..442e84a --- /dev/null +++ b/tests/materi/test_materi_readwrite.py @@ -0,0 +1,114 @@ +import pytest +from pages.materi_readwrite_page import MateriReadWritePage +from selenium.webdriver.support import expected_conditions as EC + +class TestMateriReadWrite: + + def test_readwrite_page_display_text_materi(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + text = page.get_text_materi() + assert len(text) > 0 + + def test_submit_rangkuman_success(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + rangkuman_valid = ( + "Enkapsulasi adalah konsep dalam pemrograman berorientasi objek " + "yang digunakan untuk membungkus data dan method dalam satu kelas. " + "Tujuannya adalah melindungi data agar tidak diakses langsung dari luar." + ) + page.input_rangkuman(rangkuman_valid) + page.submit_rangkuman() + + alert = page.success_message_displayed() + assert "berhasil" in alert.text.lower() + + def test_submit_rangkuman_empty_should_fail(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + page.input_rangkuman("") + page.submit_rangkuman() + + # tetap di halaman yang sama (validasi HTML required) + assert "readwrite" in driver.current_url + + def test_tugas_rangkuman_section_label_displayed(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + label = page.wait.until( + EC.visibility_of_element_located(page.TUGAS_RANGKUMAN_LABEL) + ) + + assert label.is_displayed() + assert "TUGAS RANGKUMAN" in label.text + + def test_input_rangkuman_textarea_displayed(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + textarea = page.wait.until( + EC.visibility_of_element_located(page.RANGKUMAN_TEXTAREA) + ) + + assert textarea.is_displayed() + assert textarea.is_enabled() + + def test_submit_rangkuman_less_than_50_words_should_be_rejected(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + short_text = "Enkapsulasi adalah konsep OOP untuk melindungi data." + page.input_rangkuman(short_text) + page.submit_rangkuman() + + assert page.error_message_displayed() + + def test_submit_rangkuman_more_than_75_words_should_be_rejected(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + long_text = " ".join(["enkapsulasi"] * 80) + page.input_rangkuman(long_text) + page.submit_rangkuman() + + assert page.error_message_displayed() + + def test_cannot_leave_page_without_submit_rangkuman(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + page.input_rangkuman("Rangkuman belum dikirim") + page.go_to_visual() + + # Sistem HARUS menahan user di halaman ini + assert "readwrite" in driver.current_url + + def test_textarea_cleared_after_submit(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + page = MateriReadWritePage(driver) + page.page_loaded() + + text = " ".join(["enkapsulasi"] * 50) + page.input_rangkuman(text) + page.submit_rangkuman() + + textarea = driver.find_element(*page.RANGKUMAN_TEXTAREA) + assert textarea.get_attribute("value") == "" + + + + + diff --git a/tests/materi/test_materi_visual.py b/tests/materi/test_materi_visual.py new file mode 100644 index 0000000..140b5c7 --- /dev/null +++ b/tests/materi/test_materi_visual.py @@ -0,0 +1,41 @@ +import pytest +from pages.materi_page import MateriPage + + +class TestMateriVisual: + + def test_open_visual_material(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/visual/1") + + page = MateriPage(driver) + page.visual_page_loaded() + page.sidebar_visible() + + assert "materi/visual" in driver.current_url + + def test_visual_has_image(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/visual/1") + + page = MateriPage(driver) + page.visual_page_loaded() + + assert page.driver.find_element(*page.VISUAL_IMAGE).is_displayed() + + def test_navigate_visual_to_auditory(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/visual/1") + + page = MateriPage(driver) + page.go_to_auditory() + page.auditory_page_loaded() + + assert "materi/auditory" in driver.current_url + + #@pytest.mark.xfail(reason="Belum ada validasi sebelum meninggalkan halaman materi") + def test_navigation_requires_confirmation(self, driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/visual/1") + + page = MateriPage(driver) + page.go_to_auditory() + + # Expected behavior (yang benar secara requirement) + assert "materi/visual" in driver.current_url diff --git a/tests/profile/test_profile_angkatan_validation.py b/tests/profile/test_profile_angkatan_validation.py new file mode 100644 index 0000000..cad675d --- /dev/null +++ b/tests/profile/test_profile_angkatan_validation.py @@ -0,0 +1,34 @@ +import pytest +from datetime import datetime +from pages.profile_page import ProfilePage + + + # ====================================================== + # ANGKATAN VALIDATION + # ====================================================== + +class TestProfileAngkatanValidation: + + def test_angkatan_dropdown_options(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + + current_year = datetime.now().year + + expected_years = [ + str(current_year - i) for i in reversed(range(7)) + ] + + actual_years = page.get_all_angkatan_options() + + assert actual_years == expected_years + + def test_angkatan_valid(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + + page.select_angkatan("2022") + page.click_save() + page.wait_until_reload_after_save() + + assert page.get_selected_angkatan() == "2022" \ No newline at end of file diff --git a/tests/profile/test_profile_buttons.py b/tests/profile/test_profile_buttons.py new file mode 100644 index 0000000..b2c541b --- /dev/null +++ b/tests/profile/test_profile_buttons.py @@ -0,0 +1,31 @@ +import pytest +from pages.profile_page import ProfilePage + +class TestProfileButton: + + def test_button_simpan_update_data(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + new_name = "Ini Testing" + page.set_nama_lengkap(new_name) + page.click_save() + page.wait_until_reload_after_save() + assert page.get_nama_lengkap() == new_name + + + def test_button_simpan_validasi_gagal_revert(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("") + page.click_save() + assert page.get_nama_lengkap() == default_name + + + def test_button_batal_kembali_ke_default(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("Nama Tidak Disimpan") + page.click_cancel() + assert page.get_nama_lengkap() == default_name diff --git a/tests/profile/test_profile_display.py b/tests/profile/test_profile_display.py new file mode 100644 index 0000000..0140236 --- /dev/null +++ b/tests/profile/test_profile_display.py @@ -0,0 +1,70 @@ +import pytest +from pages.profile_page import ProfilePage + +class TestProfileDisplay: + + def test_email_tampil_sesuai_akun(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Verifikasi email tampil sesuai akun dan tidak kosong + """ + page.wait_until_page_loaded() + + email = page.get_email() + + assert email is not None + #assert email != "" + assert "@" in email + assert email.endswith(".ac.id") + #or email.endswith(".com") + + + def test_email_tidak_bisa_diedit(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Verifikasi email memiliki attribute readonly + """ + assert page.is_email_readonly() is True + + + def test_nim_tampil_sesuai_akun(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Verifikasi NIM tampil dan tidak kosong + """ + nim = page.get_nim() + + assert nim is not None + assert nim != "" + assert len(nim) >= 9 + assert len(nim) <= 10 + + + def test_semester_tampil_sesuai_akun(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Verifikasi semester memiliki selected value + """ + semester = page.get_selected_semester() + + assert semester is not None + assert semester.isdigit() + assert int(semester) >= 1 + assert int(semester) <= 8 + + + def test_angkatan_tampil_sesuai_akun(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Verifikasi angkatan memiliki selected value + """ + angkatan = page.get_selected_angkatan() + + assert angkatan is not None + assert angkatan.isdigit() + assert len(angkatan) == 4 diff --git a/tests/profile/test_profile_nama_validation.py b/tests/profile/test_profile_nama_validation.py new file mode 100644 index 0000000..38640e0 --- /dev/null +++ b/tests/profile/test_profile_nama_validation.py @@ -0,0 +1,64 @@ +import pytest +from datetime import datetime +from pages.profile_page import ProfilePage + + +class TestProfileNamaValidation: + + # ====================================================== + # NAMA VALIDATION + # ====================================================== + + def test_nama_kosong(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("") + page.click_save() + assert page.get_nama_lengkap() == default_name + + def test_nama_valid(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + + page.set_nama_lengkap("Rurin Nurliza") + page.click_save() + page.wait_until_reload_after_save() + + assert page.get_nama_lengkap() == "Rurin Nurliza" + + def test_nama_kurang_5_karakter(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("Rin") + page.click_save() + assert page.get_nama_lengkap() == default_name, ("FAIL, Sistem tidak mengembalikan nama ke default") + + def test_nama_lebih_30_karakter(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("IniNamaYangSangatPanjangSekaliSeharusnyaTidakBoleh") + page.click_save() + assert page.get_nama_lengkap() == default_name, ("FAIL, Sistem tidak mengembalikan nama ke default") + + def test_nama_angka_semua(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("12345678") + page.click_save() + assert page.get_nama_lengkap() == default_name, ("FAIL, Sistem tidak mengembalikan nama ke default dan tidak memberika validasi error") + + def test_nama_simbol_semua(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("@@@@@@@") + page.click_save() + assert page.get_nama_lengkap() == default_name, ("FAIL, Sistem tidak mengembalikan nama ke default dan tidak memberika validasi error") + + + + \ No newline at end of file diff --git a/tests/profile/test_profile_nim_validation.py b/tests/profile/test_profile_nim_validation.py new file mode 100644 index 0000000..505bfe3 --- /dev/null +++ b/tests/profile/test_profile_nim_validation.py @@ -0,0 +1,53 @@ +import pytest +from datetime import datetime +from pages.profile_page import ProfilePage + + +class TestProfileNimValidation: + + # ====================================================== + # NIM VALIDATION + # ====================================================== + + def test_nim_kosong(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_nim = page.get_nim() + page.set_nim("") + page.click_save() + assert page.get_nim() == default_nim + + def test_nim_valid(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + + page.set_nim("2022012345") + page.click_save() + page.wait_until_reload_after_save() + + assert page.get_nim() == "2022012345" + + def test_nim_bukan_angka(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_nim = page.get_nim + page.set_nim("ABC12345") + page.click_save() + assert page.get_nim() == default_nim + + def test_nim_kurang_minimal(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_nim = page.get_nim + page.set_nim("1234567") + page.click_save() + assert page.get_nim() == default_nim + + def test_nim_lebih_maksimal(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + default_nim = page.get_nim + page.set_nim("1234567890123456") + page.click_save() + assert page.get_nim() == default_nim + diff --git a/tests/profile/test_profile_semester_validation.py b/tests/profile/test_profile_semester_validation.py new file mode 100644 index 0000000..06b571a --- /dev/null +++ b/tests/profile/test_profile_semester_validation.py @@ -0,0 +1,29 @@ +import pytest +from datetime import datetime +from pages.profile_page import ProfilePage + + +class TestProfileSemesterValidation: + + # ====================================================== + # SEMESTER VALIDATION + # ====================================================== + + def test_semester_valid(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + page.select_semester("5") + page.click_save() + page.wait_until_reload_after_save() + + assert page.get_selected_semester() == "5" + + def test_semester_dropdown_options(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + + options = page.get_all_semester_options() + + assert options == ["1", "2", "3", "4", "5", "6", "7", "8"] + + diff --git a/tests/profile/test_profile_upload.py b/tests/profile/test_profile_upload.py new file mode 100644 index 0000000..9afb046 --- /dev/null +++ b/tests/profile/test_profile_upload.py @@ -0,0 +1,114 @@ +import os +import pytest +from pages.profile_page import ProfilePage + + +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +TEST_DATA_DIR = os.path.join(BASE_DIR, "test_data") + +class TestProfileUpload: + + def test_upload_jpg_berhasil(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Upload file JPG berhasil dan preview berubah + """ + page.wait_until_page_loaded() + + old_src = page.get_profile_image_src() + + file_path = os.path.join(TEST_DATA_DIR, "Kucink_Jpg.jpg") + page.upload_photo(file_path) + page.click_save() + page.refresh_page() + + new_src = page.get_profile_image_src() + + assert old_src != new_src + + + def test_upload_png_berhasil(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Upload file PNG berhasil + """ + old_src = page.get_profile_image_src() + + file_path = os.path.join(TEST_DATA_DIR, "Kucink_PNG.png") + page.upload_photo(file_path) + page.click_save() + page.refresh_page() + + new_src = page.get_profile_image_src() + + assert old_src != new_src + + + def test_upload_file_selain_gambar_ditolak(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Upload file selain JPG/PNG harus ditolak + """ + old_src = page.get_profile_image_src() + + file_path = os.path.join(TEST_DATA_DIR, "invalid_file.pdf") + page.upload_photo(file_path) + page.click_save() + page.refresh_page() + + new_src = page.get_profile_image_src() + + assert old_src == new_src + + + def test_upload_file_lebih_800kb_ditolak(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Upload file > 800KB harus gagal + """ + old_src = page.get_profile_image_src() + + file_path = os.path.join(TEST_DATA_DIR, "large_image.jpg") + page.upload_photo(file_path) + page.click_save() + page.refresh_page() + + new_src = page.get_profile_image_src() + + assert old_src == new_src + + + def test_preview_muncul_setelah_upload(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Preview image harus berubah sebelum save + """ + old_src = page.get_profile_image_src() + + file_path = os.path.join(TEST_DATA_DIR, "Kucink_Jpg.jpg") + page.upload_photo(file_path) + + new_src = page.get_profile_image_src() + + assert old_src != new_src + + + def test_reset_mengembalikan_foto_default(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/profile") + page = ProfilePage(driver) + """ + Reset harus mengembalikan foto ke default + """ + page.wait_until_page_loaded() + + page.click_reset_photo() + page.refresh_page() + + src = page.get_profile_image_src() + + assert "defaultProfile" in src diff --git a/tests/register/test_register_angkatan.py b/tests/register/test_register_angkatan.py new file mode 100644 index 0000000..ec4fa4b --- /dev/null +++ b/tests/register/test_register_angkatan.py @@ -0,0 +1,75 @@ +import pytest +from datetime import datetime + + +@pytest.mark.usefixtures("driver") +class TestRegisterAngkatan: + """ + RULE ANGKATAN: + - Wajib diisi (HTML5) + - Hanya angka + - Minimal 2021 + - Maksimal tahun sekarang + 1 + """ + + # ========================= + # NEGATIVE TEST CASES + # ========================= + + def test_angkatan_kosong(self, register_page, valid_register_data): + valid_register_data["angkatan"] = "" + register_page.fill_form(valid_register_data) + assert register_page.is_field_required("angkatan") + + def test_angkatan_hanya_spasi(self, register_page, valid_register_data): + valid_register_data["angkatan"] = " " + register_page.fill_form(valid_register_data) + assert register_page.is_field_required("angkatan") + + def test_angkatan_huruf(self, register_page, valid_register_data): + valid_register_data["angkatan"] = "abcd" + register_page.fill_form(valid_register_data) + value = register_page.get_value("angkatan") + assert value == "" + + def test_angkatan_simbol(self, register_page, valid_register_data): + valid_register_data["angkatan"] = "&&&&" + register_page.fill_form(valid_register_data) + value = register_page.get_value("angkatan") + assert value == "" + + def test_angkatan_kurang_dari_minimum(self, register_page, valid_register_data): + valid_register_data["angkatan"] = "2019" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_html5_validation("angkatan") + + def test_angkatan_terlalu_besar(self, register_page, valid_register_data): + valid_register_data["angkatan"] = "9999" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_html5_validation("angkatan") + + + # ========================= + # POSITIVE TEST CASES + # ========================= + + def test_angkatan_valid_minimum(self, register_page, valid_register_data): + valid_register_data["angkatan"] = "2022" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_angkatan_valid_tahun_sekarang(self, register_page, valid_register_data): + tahun_sekarang = str(datetime.now().year) + valid_register_data["angkatan"] = tahun_sekarang + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_angkatan_valid_random(self, register_page, valid_register_data): + valid_register_data["angkatan"] = valid_register_data["angkatan"] + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() diff --git a/tests/register/test_register_email.py b/tests/register/test_register_email.py new file mode 100644 index 0000000..5350c87 --- /dev/null +++ b/tests/register/test_register_email.py @@ -0,0 +1,147 @@ +import pytest + + +@pytest.mark.usefixtures("driver") +class TestRegisterEmail: + """ + RULE EMAIL: + - Wajib diisi (HTML5) + - Harus format email valid (HTML5) + - Tidak boleh mengandung spasi + - Domain wajib: + @polije.ac.id + @student.polije.ac.id + - Validasi domain & email aktif menggunakan backend / JS + """ + + # ========================= + # NEGATIVE TEST CASES + # ========================= + + def test_email_kosong(self, register_page, valid_register_data): + valid_register_data["email"] = "" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_field_required("email") + + def test_email_hanya_spasi(self, register_page, valid_register_data): + valid_register_data["email"] = " " + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_field_required("email") + + def test_email_tanpa_at(self, register_page, valid_register_data): + valid_register_data["email"] = "userpolije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_html5_validation("email") + + def test_email_angka_semua(self, register_page, valid_register_data): + valid_register_data["email"] = "123456789" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_html5_validation("email") + + def test_nama_email_angka_semua(self, register_page, valid_register_data): + valid_register_data["email"] = "12345678@student.polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Email") + + def test_email_simbol_semua(self, register_page, valid_register_data): + valid_register_data["email"] = "&&&&" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_html5_validation("email") + + def test_email_mengandung_spasi(self, register_page, valid_register_data): + valid_register_data["email"] = "user @polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_html5_validation("email") + + def test_email_domain_bukan_polije(self, register_page, valid_register_data): + valid_register_data["email"] = "user@gmail.com" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Email") + + def test_email_domain_salah(self, register_page, valid_register_data): + valid_register_data["email"] = "user@student.polije.co.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Email") + + def test_email_terdaftar_typo(self, register_page, valid_register_data): + valid_register_data["email"] = "e41222052@polije.acid" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Email") + + def test_email_tidak_aktif(self, register_page, valid_register_data): + valid_register_data["email"] = "email.tidak.aktif@student.polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Email") + + def test_email_mengandung_dash(self, register_page, valid_register_data): + valid_register_data["email"] = "user-test@student.polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Email") + + def test_email_mengandung_plus(self, register_page, valid_register_data): + valid_register_data["email"] = "user+test@student.polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Email") + + + def test_register_dua_kali_email_sama(self, register_page, valid_register_data): + email = valid_register_data["email"] + + # Register pertama + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + # Register kedua → email sama, nama & NIM beda + register_page.open() + + data_kedua = valid_register_data.copy() + data_kedua["email"] = email + data_kedua["nama_lengkap"] = "User Kedua Unik" + data_kedua["nim"] = "E41229999" + + register_page.fill_form(data_kedua) + register_page.submit() + + assert register_page.has_error("Email") + + + # ========================= + # POSITIVE TEST CASES + # ========================= + + def test_email_mengandung_underscore(self, register_page, valid_register_data): + valid_register_data["email"] = "user_test@student.polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_email_valid_polije(self, register_page, valid_register_data): + valid_register_data["email"] = "dosen@polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_email_valid_student_polije(self, register_page, valid_register_data): + valid_register_data["email"] = "E41222050@student.polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + + + + diff --git a/tests/register/test_register_konfirmasi_password.py b/tests/register/test_register_konfirmasi_password.py new file mode 100644 index 0000000..6b36e49 --- /dev/null +++ b/tests/register/test_register_konfirmasi_password.py @@ -0,0 +1,87 @@ +import pytest + + +@pytest.mark.usefixtures("driver") +class TestRegisterKonfirmasiPassword: + """ + RULE KONFIRMASI PASSWORD: + - Wajib diisi + - Harus sama dengan password + - Validasi muncul setelah klik submit + """ + + # ========================= + # NEGATIVE TEST CASES + # ========================= + + def test_konfirmasi_password_kosong(self, register_page, valid_register_data): + """ + Konfirmasi password kosong → HTML5 required + """ + valid_register_data["password"] = "Abcdef1@" + valid_register_data["konfirmasi_password"] = "" + + register_page.fill_form(valid_register_data) + + assert register_page.is_field_required("konfirmasi_password") + + def test_konfirmasi_password_hanya_spasi(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef1@" + valid_register_data["konfirmasi_password"] = " " + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_konfirmasi_password_tidak_sama(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef1@" + valid_register_data["konfirmasi_password"] = "Abcdef2@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_konfirmasi_password_lebih_pendek(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef1@" + valid_register_data["konfirmasi_password"] = "Abcdef1" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_konfirmasi_password_lebih_panjang(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef1@" + valid_register_data["konfirmasi_password"] = "Abcdef1@xxx" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + # ========================= + # POSITIVE TEST CASES + # ========================= + + def test_konfirmasi_password_sama(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef1@" + valid_register_data["konfirmasi_password"] = "Abcdef1@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() + + def test_konfirmasi_password_valid_dengan_spasi(self, register_page, valid_register_data): + """ + Jika password mengandung spasi dan konfirmasi sama → valid + """ + valid_register_data["password"] = "Abc def1@" + valid_register_data["konfirmasi_password"] = "Abc def1@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() diff --git a/tests/register/test_register_nama_lengkap.py b/tests/register/test_register_nama_lengkap.py new file mode 100644 index 0000000..0fca588 --- /dev/null +++ b/tests/register/test_register_nama_lengkap.py @@ -0,0 +1,100 @@ +import pytest +from pages.register_page import RegisterPage + + +@pytest.mark.usefixtures("driver") +class TestRegisterNamaLengkap: + """ + RULE FINAL NAMA LENGKAP: + - Minimal 5 karakter + - Maksimal 30 karakter + - HANYA boleh huruf dan spasi + - Huruf kapital/kecil bebas + - TIDAK BOLEH angka atau simbol + """ + + # ========================= + # NEGATIVE TEST CASES + # ========================= + + def test_nama_kosong(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_field_required("nama_lengkap") + + def test_nama_hanya_spasi(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = " " + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Nama Lengkap") + + def test_nama_kurang_dari_5_karakter(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "Aa" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Nama Lengkap") + + def test_nama_lebih_dari_30_karakter(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "B" * 31 + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Nama Lengkap") + + def test_nama_angka_semua(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "98076545" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Nama Lengkap") + + def test_nama_simbol_semua(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "######" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Nama Lengkap") + + def test_nama_huruf_dan_angka(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "Bagus123" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Nama Lengkap") + + def test_nama_huruf_dan_simbol(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "Gilang@Putra" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("Nama Lengkap") + + # ========================= + # POSITIVE TEST CASES + # ========================= + + def test_nama_valid_huruf_kecil(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "gilang bagus" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_nama_valid_huruf_kapital(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "GILANG RAMADAN" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_nama_valid_huruf_dan_spasi(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "Gilang Rama" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_nama_valid_minimal_karakter(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "Rurin Nur" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_nama_valid_maksimal_karakter(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "C" * 30 + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() diff --git a/tests/register/test_register_nim.py b/tests/register/test_register_nim.py new file mode 100644 index 0000000..2c59fd6 --- /dev/null +++ b/tests/register/test_register_nim.py @@ -0,0 +1,96 @@ +import pytest + + +@pytest.mark.usefixtures("driver") +class TestRegisterNIM: + """ + RULE FINAL NIM: + - Tidak boleh kosong (HTML5 required) + - Tidak boleh spasi + - Harus kombinasi huruf + angka + - Tidak boleh karakter khusus + - Minimal 9 karakter + - Maksimal 10 karakter + """ + + # ========================= + # REQUIRED (HTML5) + # ========================= + + def test_nim_kosong(self, register_page, valid_register_data): + valid_register_data["nim"] = "" + register_page.fill_form(valid_register_data) + + # āŒ JANGAN submit + assert register_page.is_field_required("NIM") + + # ========================= + # NEGATIVE (SERVER / JS) + # ========================= + + def test_nim_spasi_saja(self, register_page, valid_register_data): + valid_register_data["nim"] = " " + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("NIM") + + def test_nim_mengandung_spasi(self, register_page, valid_register_data): + valid_register_data["nim"] = "E 4122987" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("NIM") + + def test_nim_hanya_angka(self, register_page, valid_register_data): + valid_register_data["nim"] = "412227890" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("NIM") + + def test_nim_hanya_huruf(self, register_page, valid_register_data): + valid_register_data["nim"] = "ABCDEFGHI" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("NIM") + + def test_nim_karakter_khusus(self, register_page, valid_register_data): + valid_register_data["nim"] = "E41222@89" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("NIM") + + def test_nim_kurang_dari_9_karakter(self, register_page, valid_register_data): + valid_register_data["nim"] = "E41222" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("NIM") + + def test_nim_lebih_dari_10_karakter(self, register_page, valid_register_data): + valid_register_data["nim"] = "E41222789099" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("NIM") + + # ========================= + # POSITIVE + # ========================= + + def test_nim_valid_9_karakter(self, register_page, valid_register_data): + valid_register_data["nim"] = "E41222050" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() + + def test_nim_valid_10_karakter(self, register_page, valid_register_data): + valid_register_data["nim"] = "E412227890" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() diff --git a/tests/register/test_register_password.py b/tests/register/test_register_password.py new file mode 100644 index 0000000..63f82d8 --- /dev/null +++ b/tests/register/test_register_password.py @@ -0,0 +1,160 @@ +import pytest + + +@pytest.mark.usefixtures("driver") +class TestRegisterPassword: + """ + RULE PASSWORD: + - Wajib diisi (HTML5) + - Minimal 8 karakter + - Maksimal 12 karakter + - Boleh mengandung spasi + - Wajib mengandung: + - Huruf besar + - Huruf kecil + - Angka atau karakter khusus + - Validasi muncul setelah klik submit + """ + + # ========================= + # HTML5 VALIDATION + # ========================= + + def test_password_kosong(self, register_page, valid_register_data): + """ + Password kosong → HTML5 required + """ + valid_register_data["password"] = "" + valid_register_data["konfirmasi_password"] = "" + + register_page.fill_form(valid_register_data) + + # TIDAK perlu submit, HTML5 sudah aktif + assert register_page.is_field_required("password") + + # ========================= + # NEGATIVE TEST CASES (CUSTOM VALIDATION) + # ========================= + + def test_password_kurang_dari_minimum(self, register_page, valid_register_data): + valid_register_data["password"] = "Ab1@" + valid_register_data["konfirmasi_password"] = "Ab1@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_password_lebih_dari_maksimum(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef1@xxxx" + valid_register_data["konfirmasi_password"] = "Abcdef1@xxxx" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_password_tanpa_huruf_kapital(self, register_page, valid_register_data): + valid_register_data["password"] = "abcdef1@" + valid_register_data["konfirmasi_password"] = "abcdef1@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_password_tanpa_huruf_kecil(self, register_page, valid_register_data): + valid_register_data["password"] = "ABCDEF1@" + valid_register_data["konfirmasi_password"] = "ABCDEF1@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_password_tanpa_angka(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef@@" + valid_register_data["konfirmasi_password"] = "Abcdef@@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_password_tanpa_karakter_khusus(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcdef12" + valid_register_data["konfirmasi_password"] = "Abcdef12" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + @pytest.mark.parametrize( + "password", + [ + "12345678", # angka semua + "abcdefgh", # huruf kecil semua + "ABCDEFGH", # huruf besar semua + "&&&&&&&&", # simbol semua + ] + ) + def test_password_komposisi_tidak_valid( + self, register_page, valid_register_data, password + ): + valid_register_data["password"] = password + valid_register_data["konfirmasi_password"] = password + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.has_error("Password") + + def test_password_mengandung_spasi(self, register_page, valid_register_data): + """ + Spasi BOLEH, selama rule lain terpenuhi + """ + valid_register_data["password"] = "Abc def1@" + valid_register_data["konfirmasi_password"] = "Abc def1@" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() + + def test_password_tepat_minimum(self, register_page, valid_register_data): + valid_register_data["password"] = "Abcd1@xy" + valid_register_data["konfirmasi_password"] = "Abcd1@xy" + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() + + def test_password_tepat_maksimum(self, register_page, valid_register_data): + """ + Boundary value: tepat 12 karakter + """ + valid_register_data["password"] = "Abcd12@xyzQ" + valid_register_data["konfirmasi_password"] = "Abcd12@xyzQ" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() + + def test_password_valid_kriteria(self, register_page, valid_register_data): + """ + Boundary value: tepat 8 karakter + """ + valid_register_data["password"] = "#Qwerty123" + valid_register_data["konfirmasi_password"] = "#Qwerty123" + + register_page.fill_form(valid_register_data) + register_page.submit() + + assert register_page.is_register_success() + # ========================= + # POSITIVE TEST CASE + # ========================= + + diff --git a/tests/register/test_register_semester.py b/tests/register/test_register_semester.py new file mode 100644 index 0000000..dbe3438 --- /dev/null +++ b/tests/register/test_register_semester.py @@ -0,0 +1,73 @@ +import pytest + + +@pytest.mark.usefixtures("driver") +class TestRegisterSemester: + """ + RULE SEMESTER: + - Wajib diisi (HTML5) + - Hanya angka + - Range 1 - 14 + """ + + # ========================= + # NEGATIVE TEST CASES + # ========================= + + def test_semester_kosong(self, register_page, valid_register_data): + valid_register_data["semester"] = "" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_field_required("semester") + + def test_semester_hanya_spasi(self, register_page, valid_register_data): + valid_register_data["semester"] = " " + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_field_required("semester") + + def test_semester_huruf(self, register_page, valid_register_data): + valid_register_data["semester"] = "abc" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("semester") + + def test_semester_simbol(self, register_page, valid_register_data): + valid_register_data["semester"] = "&&" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("semester") + + def test_semester_kurang_dari_minimum(self, register_page, valid_register_data): + valid_register_data["semester"] = "0" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("semester") + + def test_semester_lebih_dari_maksimum(self, register_page, valid_register_data): + valid_register_data["semester"] = "15" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.has_error("semester") + + # ========================= + # POSITIVE TEST CASES + # ========================= + + def test_semester_valid_minimum(self, register_page, valid_register_data): + valid_register_data["semester"] = "1" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_semester_valid_tengah(self, register_page, valid_register_data): + valid_register_data["semester"] = "8" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_semester_valid_maksimum(self, register_page, valid_register_data): + valid_register_data["semester"] = "14" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() diff --git a/tests/regression/regression_test.py b/tests/regression/regression_test.py new file mode 100644 index 0000000..c728eb5 --- /dev/null +++ b/tests/regression/regression_test.py @@ -0,0 +1,163 @@ +import pytest +import os +from pages.register_page import RegisterPage +from pages.live_coding_page import LiveCodingPage +from pages.profile_page import ProfilePage + +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +TEST_DATA_DIR = os.path.join(BASE_DIR, "test_data") + +@pytest.mark.usefixtures("driver") +class TestRegression: + +#Regression Daftar Akun + def test_nama_valid_huruf_kecil(self, register_page, valid_register_data): + valid_register_data["nama_lengkap"] = "rurinhaliza" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_nim_valid_9_karakter(self, register_page, valid_register_data): + valid_register_data["nim"] = "E41222555" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_semester_valid_tengah(self, register_page, valid_register_data): + valid_register_data["semester"] = "4" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_angkatan_valid_random(self, register_page, valid_register_data): + valid_register_data["angkatan"] = valid_register_data["angkatan"] + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_email_valid_student_polije(self, register_page, valid_register_data): + valid_register_data["email"] = "siswa@student.polije.ac.id" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_password_valid(self, register_page, valid_register_data): + """ + Password valid (huruf besar, kecil, angka, simbol) + """ + valid_register_data["password"] = "Qwerty098" + valid_register_data["konfirmasi_password"] = "Qwerty098" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + + def test_konfirmasi_password_sama(self, register_page, valid_register_data): + valid_register_data["password"] = "Zxcvbn123" + valid_register_data["konfirmasi_password"] = "Zxcvbn123" + register_page.fill_form(valid_register_data) + register_page.submit() + assert register_page.is_register_success() + +# Regression materi kinesthetic + def test_submit_with_correct_code(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/materi/kinesthetic/1") + page = LiveCodingPage(driver) + + page.append_code2(""" + Mahasiswa + println + """) + page.submit_code() + + result = page.get_result_message() + assert "Jawaban Benar" in result + print(result) + + def test_next_button_on_first_page(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/materi/kinesthetic/1") + page = LiveCodingPage(driver) + initial_url = page.get_current_url() + + page.click_next() + + current_url = page.get_current_url() + + assert "/kinesthetic/2" in current_url + +#Regression Pofile + def test_email_tampil_sesuai_akun(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/profile") + page = ProfilePage(driver) + """ + Verifikasi email tampil sesuai akun dan tidak kosong + """ + page.wait_until_page_loaded() + + email = page.get_email() + + assert email is not None + #assert email != "" + assert "@" in email + assert email.endswith(".ac.id") + #or email.endswith(".com") + + def test_upload_file_selain_gambar_ditolak(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/profile") + page = ProfilePage(driver) + """ + Upload file selain JPG/PNG harus ditolak + """ + old_src = page.get_profile_image_src() + + file_path = os.path.join(TEST_DATA_DIR, "invalid_file.pdf") + page.upload_photo(file_path) + page.click_save() + page.refresh_page() + + new_src = page.get_profile_image_src() + + assert old_src == new_src + + def test_nama_kosong(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/profile") + page = ProfilePage(driver) + default_name = page.get_nama_lengkap() + page.set_nama_lengkap("") + page.click_save() + assert page.get_nama_lengkap() == default_name + + def test_nim_kosong(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/profile") + page = ProfilePage(driver) + default_nim = page.get_nim() + page.set_nim("") + page.click_save() + assert page.get_nim() == default_nim + + def test_semester_valid(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/profile") + page = ProfilePage(driver) + page.select_semester("1") + page.click_save() + page.wait_until_reload_after_save() + + assert page.get_selected_semester() == "1" + + def test_angkatan_valid(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/profile") + page = ProfilePage(driver) + + page.select_angkatan("2025") + page.click_save() + page.wait_until_reload_after_save() + + assert page.get_selected_angkatan() == "2025" + + def test_button_simpan_update_data(self,driver,login_as_user_belum_kuesionerNew): + driver.get("https://hypermedialearning.project2025.id/public/profile") + page = ProfilePage(driver) + new_name = "ItsTesting" + page.set_nama_lengkap(new_name) + page.click_save() + page.wait_until_reload_after_save() + assert page.get_nama_lengkap() == new_name diff --git a/tests/resume/test_resume_pembelajaran.py b/tests/resume/test_resume_pembelajaran.py new file mode 100644 index 0000000..2a72843 --- /dev/null +++ b/tests/resume/test_resume_pembelajaran.py @@ -0,0 +1,50 @@ +import pytest +from pages.resume_pembelajaran_page import ResumePembelajaranPage +from pages.materi_readwrite_page import MateriReadWritePage + +class TestResumePembelajaran: + + def test_resume_empty_state_displayed(self,driver,login_as_user_belum_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/resume-pembelajaran") + page = ResumePembelajaranPage(driver) + page.page_loaded() + + assert page.is_empty_message_displayed() is True + + def test_resume_created_after_submit_rangkuman(self,driver, login_as_user_sudah_kuesioner): + # 1. Kirim rangkuman dari Materi Read/Write + driver.get("https://hypermedialearning.sanggadewa.my.id/materi/readwrite/1") + materi_page = MateriReadWritePage(driver) + materi_page.page_loaded() + + rangkuman_text = " ".join(["test resume"] * 50) + materi_page.input_rangkuman(rangkuman_text) + materi_page.submit_rangkuman() + + # 2. Buka Resume Pembelajaran + driver.get( + "https://hypermedialearning.sanggadewa.my.id/resume-pembelajaran" + ) + + resume_page = ResumePembelajaranPage(driver) + resume_page.page_loaded() + + resume_content = resume_page.get_resume_text() + assert rangkuman_text in resume_content + + def test_resume_persist_after_page_reload(self,driver, login_as_user_sudah_kuesioner): + driver.get("https://hypermedialearning.sanggadewa.my.id/resume-pembelajaran") + page = ResumePembelajaranPage(driver) + page.page_loaded() + + first_load_text = page.get_resume_text() + + driver.refresh() + page.page_loaded() + + second_load_text = page.get_resume_text() + assert first_load_text == second_load_text + + + + diff --git a/tests/test_data/Kucink_Jpg.jpg b/tests/test_data/Kucink_Jpg.jpg new file mode 100644 index 0000000..f4dfaf7 Binary files /dev/null and b/tests/test_data/Kucink_Jpg.jpg differ diff --git a/tests/test_data/invalid_file.pdf b/tests/test_data/invalid_file.pdf new file mode 100644 index 0000000..e236dee --- /dev/null +++ b/tests/test_data/invalid_file.pdf @@ -0,0 +1,4 @@ +INI FILE + UNTUK +TESTING + diff --git a/tests/test_data/kucink_PNG.png b/tests/test_data/kucink_PNG.png new file mode 100644 index 0000000..9c319a5 Binary files /dev/null and b/tests/test_data/kucink_PNG.png differ diff --git a/tests/test_data/large_image.jpg b/tests/test_data/large_image.jpg new file mode 100644 index 0000000..077441e Binary files /dev/null and b/tests/test_data/large_image.jpg differ diff --git a/tests/test_register.py b/tests/test_register.py new file mode 100644 index 0000000..9e97e64 --- /dev/null +++ b/tests/test_register.py @@ -0,0 +1,146 @@ +from pages.register_page import RegisterPage +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import time + + +def test_register_valid_data(driver): + """ + Test Scenario: + Periksa sistem jika user mengisi seluruh form pendaftaran dengan data valid + + Technical Requirement: + T1–T6, T7–T12, T13–T20, T21–T28, T29–T41 + + Expected Result: + Sistem menerima pendaftaran akun + """ + + register = RegisterPage(driver) + register.open() + + # === DATA UNIK === + timestamp = int(time.time()) + email_unik = f"dummy_{timestamp}@polije.ac.id" + nim_unik = f"E41{timestamp % 100000}" + + register.input_nama_lengkap("Budi Santoso") + register.input_nim("E41234567") + register.input_semester("6") + register.input_angkatan("2022") + register.input_email("dummy_test@polije.ac.id") + register.input_password("Password1!") + register.input_konfirmasi_password("Password1!") + + register.submit() + + # === WAIT PROSES SUBMIT === + WebDriverWait(driver, 10).until( + lambda d: d.current_url != RegisterPage.URL + or len(register.get_error_message()) > 0 + ) + + # === ASSERTION === + assert "register" not in driver.current_url, \ + "Registrasi gagal, user masih berada di halaman register" + +def test_register_nama_lengkap_kosong(driver): + """ + Test Scenario: + Nama Lengkap tidak diisi + + Technical Requirement: + T1 – Nama Lengkap tidak boleh kosong + + Expected Result: + Sistem menolak pendaftaran dan menampilkan validasi wajib isi + """ + + register = RegisterPage(driver) + register.open() + + # Nama kosong + register.input_nim("E41234567") + register.input_semester("6") + register.input_angkatan("2022") + register.input_email("test@polije.ac.id") + register.input_password("Password1!") + register.input_konfirmasi_password("Password1!") + + register.submit() + + assert register.is_field_invalid(RegisterPage.NAMA_LENGKAP) + assert "fill out" in register.get_validation_message(RegisterPage.NAMA_LENGKAP).lower() + +def test_register_email_tidak_valid(driver): + register = RegisterPage(driver) + register.open() + + register.input_nama_lengkap("Budi Santoso") + register.input_nim("E41234568") + register.input_semester("6") + register.input_angkatan("2022") + register.input_email("email-salah-format") + register.input_password("Password1!") + register.input_konfirmasi_password("Password1!") + + register.submit() + + assert register.is_field_invalid(RegisterPage.EMAIL) + +def test_register_password_tidak_sama(driver): + register = RegisterPage(driver) + register.open() + + register.input_nama_lengkap("Budi Santoso") + register.input_nim("E41234569") + register.input_semester("6") + register.input_angkatan("2022") + register.input_email("passwordbeda@polije.ac.id") + register.input_password("Password1!") + register.input_konfirmasi_password("Password2!") + + register.submit() + + errors = register.get_error_message() + assert len(errors) > 0 + +def test_register_nim_kosong(driver): + register = RegisterPage(driver) + register.open() + + register.input_nama_lengkap("Budi Santoso") + register.input_semester("6") + register.input_angkatan("2022") + register.input_email("nimkosong@polije.ac.id") + register.input_password("Password1!") + register.input_konfirmasi_password("Password1!") + + register.submit() + + assert register.is_field_invalid(RegisterPage.NIM) + +def test_register_semester_bukan_angka(driver): + register = RegisterPage(driver) + register.open() + + register.input_nama_lengkap("Budi Santoso") + register.input_nim("E41234570") + register.input_semester("enam") + register.input_angkatan("2022") + register.input_email("semester@polije.ac.id") + register.input_password("Password1!") + register.input_konfirmasi_password("Password1!") + + register.submit() + + assert register.is_field_invalid(RegisterPage.SEMESTER) + +def test_register_semua_field_kosong(driver): + register = RegisterPage(driver) + register.open() + + # ASSERTION TANPA CLICK + assert register.is_field_invalid(RegisterPage.NAMA_LENGKAP) + + diff --git a/utils/data_generator.py b/utils/data_generator.py new file mode 100644 index 0000000..099f7c8 --- /dev/null +++ b/utils/data_generator.py @@ -0,0 +1,61 @@ +import random +import string +import time + + +def random_string(length=6): + return ''.join(random.choices(string.ascii_letters, k=length)) + + +def random_number(length=10): + return ''.join(random.choices(string.digits, k=length)) + + +def valid_nama_lengkap(): + return f"User {random_string(5)}" + + +def valid_nim(): + return random_number(9) + + +def valid_semester(): + return str(random.randint(1, 8)) + + +def valid_angkatan(): + return str(random.randint(2022, 2025)) + + +def valid_email(): + timestamp = int(time.time() * 1000) + return f"user{timestamp}@student.polije.ac.id" + + +def valid_password(): + return "Test@1234" + + +def generate_valid_register_data(): + """ + Data VALID & lengkap untuk form register + """ + return { + "nama": valid_nama_lengkap(), + "nim": valid_nim(), + "semester": valid_semester(), + "angkatan": valid_angkatan(), + "email": valid_email(), + "password": valid_password(), + } + +def generate_valid_nim(): + """ + NIM valid: + - huruf + angka + - panjang 9–10 + Contoh: E41222789 + """ + prefix = random.choice(string.ascii_uppercase) + digits = ''.join(random.choices(string.digits, k=random.randint(8, 9))) + return prefix + digits