# 🧪 Checklist Học Selenium 6 Tuần - Dành Cho Tester Fresher

> **Hướng dẫn sử dụng:** Copy file này vào Notion, Obsidian, hoặc bất kỳ app hỗ trợ Markdown. Tick vào checkbox khi hoàn thành từng mục. Đừng bỏ qua bài tập thực hành — đó là phần quan trọng nhất!

---

## 📋 Tổng quan lộ trình

| Tuần | Chủ đề | Mức độ |
|------|--------|--------|
| Tuần 1 | Cài đặt môi trường & Hello World | 🟢 Dễ |
| Tuần 2 | Locators & Tìm phần tử | 🟢 Dễ |
| Tuần 3 | Waits & Xử lý tình huống đặc biệt | 🟡 Trung bình |
| Tuần 4 | Page Object Model | 🟡 Trung bình |
| Tuần 5 | pytest Integration | 🟡 Trung bình |
| Tuần 6 | Report, Parallel & CI/CD | 🔴 Khó hơn |

---

## ✅ TUẦN 1 — Cài Đặt, Hello World & WebDriver

> **Mục tiêu:** Chạy được script Selenium đầu tiên, hiểu cách browser driver hoạt động.

### 📚 Kiến thức cần học

- [ ] Selenium là gì? Tại sao dùng Selenium cho automation testing?
- [ ] Sự khác biệt giữa Selenium IDE, Selenium WebDriver, Selenium Grid
- [ ] Python basics cần thiết cho Selenium (nếu chưa biết): biến, hàm, import
- [ ] Hiểu khái niệm WebDriver — cầu nối giữa code và browser
- [ ] ChromeDriver là gì? Tại sao version phải khớp với Chrome?
- [ ] GeckoDriver cho Firefox hoạt động như thế nào?
- [ ] Cấu trúc một script Selenium cơ bản:
  - `webdriver.Chrome()` / `webdriver.Firefox()`
  - `driver.get(url)`
  - `driver.quit()` vs `driver.close()`
- [ ] Các thuộc tính cơ bản:
  - `driver.title` — lấy tiêu đề trang
  - `driver.current_url` — lấy URL hiện tại
  - `driver.maximize_window()`
  - `driver.set_window_size(width, height)`

### 🛠️ Cài đặt môi trường

- [ ] Cài Python 3.8+ ([python.org](https://python.org))
- [ ] Cài pip và kiểm tra: `pip --version`
- [ ] Tạo virtual environment:
  ```bash
  python -m venv selenium_env
  # Windows:
  selenium_env\Scripts\activate
  # Mac/Linux:
  source selenium_env/bin/activate
  ```
- [ ] Cài Selenium:
  ```bash
  pip install selenium
  ```
- [ ] Cài webdriver-manager (tự động quản lý driver):
  ```bash
  pip install webdriver-manager
  ```
- [ ] Kiểm tra cài đặt thành công:
  ```python
  import selenium
  print(selenium.__version__)
  ```
- [ ] Cài VS Code hoặc PyCharm (chọn 1)
- [ ] Cài extension Python cho VS Code (nếu dùng VS Code)

### 🏋️ Bài tập thực hành

- [ ] **Bài 1.1 — Hello World đầu tiên**
  ```python
  # Mục tiêu: Mở google.com, in title ra console, đóng browser
  from selenium import webdriver
  from webdriver_manager.chrome import ChromeDriverManager
  from selenium.webdriver.chrome.service import Service

  service = Service(ChromeDriverManager().install())
  driver = webdriver.Chrome(service=service)
  driver.get("https://www.google.com")
  print("Title:", driver.title)
  print("URL:", driver.current_url)
  driver.quit()
  ```

- [ ] **Bài 1.2 — Chạy với Firefox**
  ```python
  # Mục tiêu: Làm lại Bài 1.1 nhưng dùng Firefox
  from selenium import webdriver
  from webdriver_manager.firefox import GeckoDriverManager
  from selenium.webdriver.firefox.service import Service
  # Tự hoàn thành phần còn lại...
  ```

- [ ] **Bài 1.3 — Thử nghiệm các thuộc tính**
  - Mở `https://www.wikipedia.org`
  - In ra: title, current_url
  - Maximize window
  - Chụp screenshot: `driver.save_screenshot("screenshot.png")`
  - Đóng browser

- [ ] **Bài 1.4 — Điều hướng trang**
  ```python
  # Mục tiêu: Dùng các lệnh điều hướng
  driver.get("https://www.google.com")
  driver.get("https://www.youtube.com")
  driver.back()    # Quay lại Google
  driver.forward() # Tiến tới YouTube
  driver.refresh() # Refresh trang
  ```

- [ ] **Bài 1.5 — Headless mode** (Bonus)
  ```python
  # Chạy Chrome không hiện giao diện — hữu ích khi chạy trên server
  from selenium.webdriver.chrome.options import Options
  options = Options()
  options.add_argument("--headless")
  driver = webdriver.Chrome(options=options)
  ```

### 📝 Ghi chú cá nhân tuần 1

```
Những gì tôi đã học được:
_________________________________

Những gì còn khó hiểu:
_________________________________

Thời gian hoàn thành: ___ giờ
```

### 🎯 Tiêu chí hoàn thành Tuần 1

- [ ] ✅ Chạy được script mở Chrome/Firefox và truy cập website
- [ ] ✅ Giải thích được tại sao cần WebDriver (không được nhìn tài liệu)
- [ ] ✅ Biết sự khác biệt giữa `driver.quit()` và `driver.close()`
- [ ] ✅ Chụp screenshot thành công và lưu vào file
- [ ] ✅ Chạy được Chrome ở headless mode

---

## ✅ TUẦN 2 — Locators & Tìm Phần Tử

> **Mục tiêu:** Tìm và tương tác được với bất kỳ phần tử nào trên trang web.

### 📚 Kiến thức cần học

- [ ] HTML cơ bản cần biết: `id`, `class`, `name`, `href`, `type`, `value`
- [ ] Cách dùng Chrome DevTools (F12) để inspect element
- [ ] **8 loại locator** trong Selenium:
  - [ ] `By.ID` — nhanh nhất, dùng khi có
  - [ ] `By.NAME` — cho input fields
  - [ ] `By.CLASS_NAME` — cẩn thận khi class không unique
  - [ ] `By.TAG_NAME` — ít dùng, thường trả về nhiều element
  - [ ] `By.LINK_TEXT` — cho thẻ `<a>` với text chính xác
  - [ ] `By.PARTIAL_LINK_TEXT` — cho thẻ `<a>` với text một phần
  - [ ] `By.XPATH` — mạnh nhất, dùng khi các locator khác không được
  - [ ] `By.CSS_SELECTOR` — nhanh và linh hoạt
- [ ] `find_element()` vs `find_elements()` — trả về 1 element vs list
- [ ] Các hành động với element:
  - [ ] `.click()` — click vào element
  - [ ] `.send_keys("text")` — nhập text
  - [ ] `.clear()` — xóa text trong input
  - [ ] `.text` — lấy nội dung text
  - [ ] `.get_attribute("attr")` — lấy giá trị attribute
  - [ ] `.is_displayed()` — kiểm tra có hiển thị không
  - [ ] `.is_enabled()` — kiểm tra có được click không
  - [ ] `.is_selected()` — kiểm tra checkbox/radio có được chọn không
- [ ] **XPath cơ bản:**
  - [ ] Absolute XPath: `/html/body/div/...` (tránh dùng)
  - [ ] Relative XPath: `//tagname[@attribute='value']`
  - [ ] XPath với text: `//button[text()='Submit']`
  - [ ] XPath contains: `//div[contains(@class,'error')]`
  - [ ] XPath parent/child: `//div[@id='form']//input`
- [ ] **CSS Selector cơ bản:**
  - [ ] ID: `#myId`
  - [ ] Class: `.myClass`
  - [ ] Attribute: `input[type='text']`
  - [ ] Child: `div > p`
  - [ ] Descendant: `div p`

### 🏋️ Bài tập thực hành

> 💡 **Gợi ý:** Dùng [https://www.selenium.dev/selenium/web/web-form.html](https://www.selenium.dev/selenium/web/web-form.html) để luyện tập

- [ ] **Bài 2.1 — Điền form cơ bản**
  ```python
  # Mục tiêu: Tìm input bằng các locator khác nhau, điền thông tin
  # Trang: https://www.selenium.dev/selenium/web/web-form.html
  
  # Tìm bằng name
  text_input = driver.find_element(By.NAME, "my-text")
  text_input.send_keys("Hello Selenium!")
  
  # Tìm bằng ID
  # Tự tìm element có ID trên trang và điền vào
  
  # Tìm bằng CSS Selector
  # Tự thực hành...
  ```

- [ ] **Bài 2.2 — So sánh các locator**
  - Tìm cùng 1 element bằng ít nhất 3 locator khác nhau
  - In ra kết quả để verify chúng tìm cùng 1 element
  - Ghi chú: locator nào ngắn gọn nhất? Nào dễ maintain nhất?

- [ ] **Bài 2.3 — Luyện XPath**
  ```python
  # Trang: https://www.google.com
  # Tìm search box bằng XPath
  search_box = driver.find_element(By.XPATH, "//input[@name='q']")
  search_box.send_keys("Selenium WebDriver")
  search_box.submit()
  
  # Sau khi kết quả hiện ra:
  # Tìm tất cả các link kết quả
  results = driver.find_elements(By.XPATH, "//h3")
  for i, result in enumerate(results[:5]):
      print(f"Kết quả {i+1}: {result.text}")
  ```

- [ ] **Bài 2.4 — Luyện CSS Selector**
  ```python
  # Viết lại Bài 2.3 nhưng dùng CSS Selector thay vì XPath
  # Gợi ý: input[name='q']
  ```

- [ ] **Bài 2.5 — Kiểm tra trạng thái element**
  ```python
  # Mục tiêu: Kiểm tra is_displayed(), is_enabled(), get_attribute()
  # Trang: https://www.selenium.dev/selenium/web/web-form.html
  
  # Tìm disabled input và kiểm tra
  disabled_input = driver.find_element(By.CSS_SELECTOR, "input[disabled]")
  print("Hiển thị:", disabled_input.is_displayed())
  print("Enabled:", disabled_input.is_enabled())
  print("Value:", disabled_input.get_attribute("value"))
  ```

- [ ] **Bài 2.6 — Mini project: Đăng nhập giả lập**
  ```python
  # Trang: https://the-internet.herokuapp.com/login
  # Username: tomsmith | Password: SuperSecretPassword!
  # Mục tiêu:
  # 1. Điền username và password
  # 2. Click nút Login
  # 3. Kiểm tra đăng nhập thành công bằng cách lấy text thông báo
  ```

### 📝 Bảng so sánh Locator — Điền vào sau khi học xong

| Locator | Khi nào dùng | Ưu điểm | Nhược điểm |
|---------|-------------|---------|-----------|
| ID | | | |
| Name | | | |
| XPath | | | |
| CSS Selector | | | |
| Class Name | | | |

### 🎯 Tiêu chí hoàn thành Tuần 2

- [ ] ✅ Dùng được tất cả 8 loại locator
- [ ] ✅ Viết được XPath relative (không dùng absolute XPath)
- [ ] ✅ Phân biệt được khi nào dùng `find_element` vs `find_elements`
- [ ] ✅ Hoàn thành Bài 2.6 — đăng nhập và verify thành công
- [ ] ✅ Giải thích được tại sao ID locator được ưu tiên hơn XPath

---

## ✅ TUẦN 3 — Waits & Xử Lý Tình Huống Đặc Biệt

> **Mục tiêu:** Xử lý được các tình huống phức tạp: trang load chậm, popup, iframe, dropdown.

### 📚 Kiến thức cần học

#### ⏰ Waits (Chờ đợi)

- [ ] Tại sao cần Wait? — Trang web load không đồng bộ (AJAX, API calls)
- [ ] **Implicit Wait:**
  ```python
  driver.implicitly_wait(10)  # Chờ tối đa 10 giây cho mọi find_element
  ```
  - [ ] Hiểu: chỉ set 1 lần, áp dụng cho toàn bộ session
  - [ ] Nhược điểm: không linh hoạt, không biết đang chờ điều kiện gì

- [ ] **Explicit Wait (Quan trọng nhất):**
  ```python
  from selenium.webdriver.support.ui import WebDriverWait
  from selenium.webdriver.support import expected_conditions as EC
  
  wait = WebDriverWait(driver, timeout=10)
  element = wait.until(EC.element_to_be_clickable((By.ID, "myButton")))
  ```
  - [ ] Các `expected_conditions` thường dùng:
    - [ ] `presence_of_element_located` — element có trong DOM
    - [ ] `visibility_of_element_located` — element hiển thị
    - [ ] `element_to_be_clickable` — element có thể click
    - [ ] `text_to_be_present_in_element` — element có chứa text
    - [ ] `invisibility_of_element_located` — element đã ẩn đi
    - [ ] `url_contains` — URL chứa chuỗi
    - [ ] `title_contains` — title chứa chuỗi
    - [ ] `alert_is_present` — có alert popup

- [ ] **Fluent Wait** (nâng cao):
  ```python
  wait = WebDriverWait(driver, timeout=30, poll_frequency=2)
  # Kiểm tra mỗi 2 giây, tối đa 30 giây
  ```

- [ ] **So sánh 3 loại wait** — điền vào bảng sau khi học:

| | Implicit | Explicit | Fluent |
|--|---------|---------|--------|
| Scope | | | |
| Linh hoạt | | | |
| Best practice | | | |

#### 🔔 Xử lý Alert/Popup

- [ ] Alert là gì? 3 loại: Alert, Confirm, Prompt
- [ ] Cách xử lý:
  ```python
  # Chuyển sang alert
  alert = driver.switch_to.alert
  
  # Lấy text trong alert
  print(alert.text)
  
  # Chấp nhận (OK)
  alert.accept()
  
  # Từ chối (Cancel) - chỉ dùng cho Confirm
  alert.dismiss()
  
  # Nhập text - chỉ dùng cho Prompt
  alert.send_keys("My input")
  alert.accept()
  ```

#### 🖼️ Xử lý iFrame

- [ ] iFrame là gì? Tại sao không find_element được bên trong iFrame?
- [ ] Switch vào iFrame:
  ```python
  # Bằng index (vị trí)
  driver.switch_to.frame(0)
  
  # Bằng name hoặc id
  driver.switch_to.frame("frameName")
  
  # Bằng WebElement
  iframe = driver.find_element(By.TAG_NAME, "iframe")
  driver.switch_to.frame(iframe)
  
  # Quay lại trang chính
  driver.switch_to.default_content()
  
  # Quay lại frame cha (nếu frame lồng nhau)
  driver.switch_to.parent_frame()
  ```

#### 📋 Xử lý Dropdown

- [ ] **Cách 1: Dùng class `Select`** (cho `<select>` HTML thuần):
  ```python
  from selenium.webdriver.support.ui import Select
  
  dropdown = Select(driver.find_element(By.ID, "myDropdown"))
  
  # Chọn theo visible text
  dropdown.select_by_visible_text("Option 1")
  
  # Chọn theo value attribute
  dropdown.select_by_value("opt1")
  
  # Chọn theo index (bắt đầu từ 0)
  dropdown.select_by_index(2)
  
  # Lấy option đang được chọn
  print(dropdown.first_selected_option.text)
  
  # Lấy tất cả options
  for option in dropdown.options:
      print(option.text)
  ```

- [ ] **Cách 2: Custom dropdown** (div/ul/li — không dùng `<select>`):
  ```python
  # Click vào dropdown để mở
  driver.find_element(By.ID, "customDropdown").click()
  
  # Tìm và click option mong muốn
  driver.find_element(By.XPATH, "//li[text()='Option 1']").click()
  ```

#### 🪟 Xử lý Multiple Windows/Tabs

- [ ] Chuyển đổi giữa các cửa sổ:
  ```python
  # Lấy handle của cửa sổ hiện tại
  main_window = driver.current_window_handle
  
  # Lấy tất cả handles
  all_windows = driver.window_handles
  
  # Chuyển sang cửa sổ mới
  for window in all_windows:
      if window != main_window:
          driver.switch_to.window(window)
          break
  
  # Quay lại cửa sổ chính
  driver.switch_to.window(main_window)
  ```

### 🏋️ Bài tập thực hành

- [ ] **Bài 3.1 — Luyện Explicit Wait**
  ```python
  # Trang: https://the-internet.herokuapp.com/dynamic_loading/1
  # Mục tiêu:
  # 1. Click nút "Start"
  # 2. Dùng WebDriverWait chờ element "Hello World!" xuất hiện
  # 3. In ra text của element đó
  # KHÔNG dùng time.sleep()!
  ```

- [ ] **Bài 3.2 — So sánh Implicit vs Explicit Wait**
  - Viết cùng 1 test bằng implicit wait
  - Viết lại bằng explicit wait
  - Ghi chú sự khác biệt bạn quan sát được

- [ ] **Bài 3.3 — Xử lý Alert**
  ```python
  # Trang: https://the-internet.herokuapp.com/javascript_alerts
  # Mục tiêu:
  # 1. Test "JS Alert" - click OK, verify result
  # 2. Test "JS Confirm" - click Cancel, verify result
  # 3. Test "JS Prompt" - nhập text "Xin chào", click OK, verify result
  ```

- [ ] **Bài 3.4 — Xử lý iFrame**
  ```python
  # Trang: https://the-internet.herokuapp.com/iframe
  # Mục tiêu:
  # 1. Switch vào iframe
  # 2. Xóa nội dung text editor
  # 3. Nhập text mới: "Tôi đang học Selenium!"
  # 4. Switch ra ngoài iframe
  ```

- [ ] **Bài 3.5 — Xử lý Dropdown**
  ```python
  # Trang: https://www.selenium.dev/selenium/web/web-form.html
  # Mục tiêu:
  # 1. Tìm dropdown <select>
  # 2. In ra tất cả options
  # 3. Chọn option bằng 3 cách: text, value, index
  # 4. Verify option đang được chọn
  ```

- [ ] **Bài 3.6 — Multiple Windows**
  ```python
  # Trang: https://the-internet.herokuapp.com/windows
  # Mục tiêu:
  # 1. Click "Click Here" để mở cửa sổ mới
  # 2. Switch sang cửa sổ mới
  # 3. Lấy title của cửa sổ mới
  # 4. Đóng cửa sổ mới
  # 5. Switch về cửa sổ chính
  # 6. Verify vẫn ở đúng trang
  ```

- [ ] **Bài 3.7 — Mini project tổng hợp**
  ```python
  # Trang: https://the-internet.herokuapp.com/
  # Mục tiêu: Hoàn thành 5 challenge sau chỉ trong 1 script:
  # 1. Xử lý alert (javascript_alerts)
  # 2. Xử lý iframe (iframe)
  # 3. Xử lý dropdown (dropdown)
  # 4. Xử lý multiple windows (windows)
  # 5. Dynamic loading với explicit wait
  ```

### 🎯 Tiêu chí hoàn thành Tuần 3

- [ ] ✅ Giải thích được sự khác nhau giữa implicit và explicit wait
- [ ] ✅ Hoàn thành Bài 3.1 mà không dùng `time.sleep()`
- [ ] ✅ Xử lý được 3 loại alert (alert, confirm, prompt)
- [ ] ✅ Switch vào iframe, thao tác, và switch ra thành công
- [ ] ✅ Chọn dropdown bằng cả 3 cách (text, value, index)
- [ ] ✅ Xử lý được multiple windows

---

## ✅ TUẦN 4 — Page Object Model (POM)

> **Mục tiêu:** Tái cấu trúc code theo POM pattern để dễ maintain và scale.

### 📚 Kiến thức cần học

#### 🏗️ Tại sao cần POM?

- [ ] Vấn đề với code "không có structure":
  - Locator bị duplicate ở nhiều file
  - Khi UI thay đổi → phải sửa ở nhiều chỗ
  - Code khó đọc, khó hiểu
- [ ] POM giải quyết vấn đề gì:
  - Tách **locator** và **actions** ra khỏi **test logic**
  - Mỗi page = 1 class
  - Test chỉ gọi method, không biết locator

#### 📁 Cấu trúc project chuẩn

```
selenium_project/
│
├── pages/                    # Chứa Page Objects
│   ├── __init__.py
│   ├── base_page.py          # Class cha chứa common methods
│   ├── login_page.py         # Page Object cho trang Login
│   ├── home_page.py          # Page Object cho trang Home
│   └── product_page.py       # Page Object cho trang Product
│
├── tests/                    # Chứa test cases
│   ├── __init__.py
│   ├── test_login.py
│   ├── test_home.py
│   └── test_product.py
│
├── utils/                    # Utility functions
│   ├── __init__.py
│   ├── driver_factory.py     # Tạo và config WebDriver
│   └── helpers.py            # Helper functions
│
├── test_data/                # Dữ liệu test
│   ├── test_users.json
│   └── test_config.yaml
│
├── screenshots/              # Lưu screenshot khi fail
│
├── reports/                  # HTML reports
│
├── conftest.py               # pytest fixtures (học ở Tuần 5)
├── requirements.txt          # Dependencies
└── README.md
```

#### 💻 Code mẫu POM

- [ ] **BasePage class:**
  ```python
  # pages/base_page.py
  from selenium.webdriver.support.ui import WebDriverWait
  from selenium.webdriver.support import expected_conditions as EC
  
  class BasePage:
      def __init__(self, driver):
          self.driver = driver
          self.wait = WebDriverWait(driver, timeout=10)
      
      def find(self, locator):
          return self.wait.until(
              EC.presence_of_element_located(locator)
          )
      
      def click(self, locator):
          self.wait.until(
              EC.element_to_be_clickable(locator)
          ).click()
      
      def type(self, locator, text):
          element = self.find(locator)
          element.clear()
          element.send_keys(text)
      
      def get_text(self, locator):
          return self.find(locator).text
      
      def take_screenshot(self, filename):
          self.driver.save_screenshot(f"screenshots/{filename}.png")
  ```

- [ ] **LoginPage class:**
  ```python
  # pages/login_page.py
  from selenium.webdriver.common.by import By
  from pages.base_page import BasePage
  
  class LoginPage(BasePage):
      # Locators - định nghĩa ở đây, không nằm trong test
      URL = "https://the-internet.herokuapp.com/login"
      USERNAME_INPUT = (By.ID, "username")
      PASSWORD_INPUT = (By.ID, "password")
      LOGIN_BUTTON = (By.CSS_SELECTOR, "button[type='submit']")
      SUCCESS_MSG = (By.CSS_SELECTOR, ".flash.success")
      ERROR_MSG = (By.CSS_SELECTOR, ".flash.error")
      
      def open(self):
          self.driver.get(self.URL)
          return self
      
      def enter_username(self, username):
          self.type(self.USERNAME_INPUT, username)
          return self
      
      def enter_password(self, password):
          self.type(self.PASSWORD_INPUT, password)
          return self
      
      def click_login(self):
          self.click(self.LOGIN_BUTTON)
          return self
      
      def login(self, username, password):
          """Shortcut method - đăng nhập 1 bước"""
          self.enter_username(username)
          self.enter_password(password)
          self.click_login()
          return self
      
      def get_success_message(self):
          return self.get_text(self.SUCCESS_MSG)
      
      def get_error_message(self):
          return self.get_text(self.ERROR_MSG)
  ```

- [ ] **Test file sử dụng POM:**
  ```python
  # tests/test_login.py
  from selenium import webdriver
  from webdriver_manager.chrome import ChromeDriverManager
  from selenium.webdriver.chrome.service import Service
  from pages.login_page import LoginPage
  
  def test_valid_login():
      service = Service(ChromeDriverManager().install())
      driver = webdriver.Chrome(service=service)
      
      login_page = LoginPage(driver)
      login_page.open()
      login_page.login("tomsmith", "SuperSecretPassword!")
      
      message = login_page.get_success_message()
      assert "You logged into a secure area!" in message
      
      driver.quit()
  
  def test_invalid_login():
      # Tự viết...
      pass
  ```

- [ ] **Hiểu Method Chaining** (return self pattern):
  ```python
  # Thay vì viết:
  login_page.enter_username("tomsmith")
  login_page.enter_password("password")
  login_page.click_login()
  
  # Có thể viết gọn:
  login_page.enter_username("tomsmith") \
             .enter_password("password") \
             .click_login()
  ```

#### 🔑 Các nguyên tắc POM

- [ ] Mỗi page chỉ biết về chính nó
- [ ] Locator KHÔNG được đặt trong test file
- [ ] Method nên trả về Page Object (để method chaining)
- [ ] Không có assertion trong Page Object class (để trong test)
- [ ] BasePage chứa các hành động chung

### 🏋️ Bài tập thực hành

- [ ] **Bài 4.1 — Tạo cấu trúc project**
  - Tạo thư mục theo cấu trúc đã học
  - Tạo file `requirements.txt`:
    ```
    selenium==4.x.x
    webdriver-manager==x.x.x
    pytest==x.x.x
    ```
  - Tạo `__init__.py` cho các package
  - Khởi tạo git repo, tạo `.gitignore`

- [ ] **Bài 4.2 — Viết BasePage**
  - Implement BasePage với ít nhất 8 common methods
  - Thêm logging để track actions:
    ```python
    import logging
    logger = logging.getLogger(__name__)
    
    def click(self, locator):
        logger.info(f"Clicking element: {locator}")
        self.wait.until(EC.element_to_be_clickable(locator)).click()
    ```

- [ ] **Bài 4.3 — Viết Page Objects**
  - Viết `LoginPage` cho `https://the-internet.herokuapp.com/login`
  - Viết `SecureAreaPage` (trang sau khi đăng nhập thành công)
  - Mỗi page phải có tất cả locators và methods cần thiết

- [ ] **Bài 4.4 — Viết Test Cases**
  ```python
  # Viết các test case sau dùng POM:
  # 1. Đăng nhập thành công
  # 2. Đăng nhập với password sai
  # 3. Đăng nhập với username sai
  # 4. Đăng nhập với cả hai trường trống
  # 5. Đăng nhập thành công rồi logout
  ```

- [ ] **Bài 4.5 — Refactor code cũ**
  - Lấy bài tập từ Tuần 2 và Tuần 3
  - Refactor theo POM pattern
  - So sánh trước và sau — code nào dễ đọc hơn?

- [ ] **Bài 4.6 — Mini Project (Quan trọng)**
  ```
  Trang: https://www.saucedemo.com/
  Username: standard_user | Password: secret_sauce
  
  Tạo Page Objects cho:
  - LoginPage
  - InventoryPage (trang danh sách sản phẩm)
  - CartPage (giỏ hàng)
  
  Viết test:
  1. Đăng nhập thành công
  2. Thêm sản phẩm vào giỏ hàng
  3. Kiểm tra số lượng trong giỏ hàng
  4. Vào giỏ hàng, verify sản phẩm đúng
  ```

### 🎯 Tiêu chí hoàn thành Tuần 4

- [ ] ✅ Giải thích được tại sao POM quan trọng (không xem tài liệu)
- [ ] ✅ Cấu trúc project đúng với `pages/`, `tests/`, `utils/`
- [ ] ✅ `BasePage` có ít nhất 6 common methods
- [ ] ✅ Locators không xuất hiện trong test file
- [ ] ✅ Hoàn thành Mini Project Bài 4.6 với ít nhất 4 test cases

---

## ✅ TUẦN 5 — pytest Integration

> **Mục tiêu:** Tích hợp pytest để quản lý và chạy test chuyên nghiệp.

### 📚 Kiến thức cần học

#### 🧪 pytest cơ bản

- [ ] Cài đặt: `pip install pytest pytest-html pytest-xdist`
- [ ] Convention đặt tên file test: `test_*.py` hoặc `*_test.py`
- [ ] Convention đặt tên function test: bắt đầu bằng `test_`
- [ ] Chạy test:
  ```bash
  pytest                              # Chạy tất cả
  pytest tests/test_login.py          # Chạy 1 file
  pytest tests/test_login.py::test_valid_login  # Chạy 1 test
  pytest -v                           # Verbose mode
  pytest -s                           # In output ra console
  pytest -k "login"                   # Chạy test có chữ "login"
  ```

#### ⚙️ Fixtures

- [ ] Fixture là gì? — Setup và teardown tự động
- [ ] **Fixture cơ bản:**
  ```python
  # conftest.py
  import pytest
  from selenium import webdriver
  from webdriver_manager.chrome import ChromeDriverManager
  from selenium.webdriver.chrome.service import Service
  
  @pytest.fixture
  def driver():
      """Setup: tạo WebDriver trước mỗi test"""
      service = Service(ChromeDriverManager().install())
      driver = webdriver.Chrome(service=service)
      driver.maximize_window()
      yield driver  # Test chạy ở đây
      driver.quit()  # Teardown: đóng browser sau mỗi test
  ```

- [ ] **Fixture scope:**
  ```python
  # function (default): chạy lại cho mỗi test function
  @pytest.fixture(scope="function")
  
  # class: chạy 1 lần cho mỗi test class
  @pytest.fixture(scope="class")
  
  # module: chạy 1 lần cho mỗi file test
  @pytest.fixture(scope="module")
  
  # session: chạy 1 lần cho toàn bộ test session
  @pytest.fixture(scope="session")
  ```

- [ ] **Fixture có tham số:**
  ```python
  @pytest.fixture
  def login_page(driver):
      from pages.login_page import LoginPage
      page = LoginPage(driver)
      page.open()
      return page
  
  # Test sử dụng fixture
  def test_login(login_page):
      login_page.login("tomsmith", "SuperSecretPassword!")
      assert "secure" in login_page.driver.current_url
  ```

- [ ] **Fixture cho logged-in state:**
  ```python
  @pytest.fixture
  def logged_in_driver(driver):
      """Driver đã đăng nhập sẵn"""
      from pages.login_page import LoginPage
      login_page = LoginPage(driver)
      login_page.open()
      login_page.login("tomsmith", "SuperSecretPassword!")
      yield driver
      # driver.quit() được handle bởi fixture "driver"
  ```

#### 📊 Parametrize

- [ ] **@pytest.mark.parametrize** — Chạy cùng 1 test với nhiều data:
  ```python
  import pytest
  
  @pytest.mark.parametrize("username, password, expected", [
      ("tomsmith", "SuperSecretPassword!", "success"),
      ("invalid_user", "wrong_pass", "error"),
      ("", "", "error"),
      ("tomsmith", "", "error"),
  ])
  def test_login_scenarios(driver, username, password, expected):
      from pages.login_page import LoginPage
      login_page = LoginPage(driver)
      login_page.open()
      login_page.login(username, password)
      
      if expected == "success":
          assert "You logged into" in login_page.get_success_message()
      else:
          assert login_page.get_error_message() != ""
  ```

- [ ] **Parametrize từ file external:**
  ```python
  import json
  import pytest
  
  def load_test_data():
      with open("test_data/login_data.json") as f:
          return json.load(f)
  
  @pytest.mark.parametrize("test_case", load_test_data())
  def test_login(driver, test_case):
      # test_case là dict từ JSON
      pass
  ```

#### 🏷️ Marks

- [ ] Đánh dấu test để phân loại và lọc:
  ```python
  @pytest.mark.smoke       # Test quan trọng nhất
  @pytest.mark.regression  # Test full regression
  @pytest.mark.slow        # Test chạy chậm
  
  # Chạy chỉ smoke tests:
  pytest -m smoke
  
  # Bỏ qua slow tests:
  pytest -m "not slow"
  ```

- [ ] **Skip test:**
  ```python
  @pytest.mark.skip(reason="Feature chưa implement")
  def test_payment():
      pass
  
  @pytest.mark.skipif(condition, reason="Chỉ chạy trên Windows")
  def test_windows_only():
      pass
  ```

#### 📸 Screenshot khi test fail

```python
# conftest.py
@pytest.fixture(autouse=True)
def screenshot_on_failure(request, driver):
    yield
    if request.node.rep_call.failed:
        test_name = request.node.name
        driver.save_screenshot(f"screenshots/FAILED_{test_name}.png")

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    setattr(item, "rep_" + rep.when, rep)
```

### 🏋️ Bài tập thực hành

- [ ] **Bài 5.1 — Setup conftest.py**
  - Tạo `conftest.py` tại root project
  - Implement `driver` fixture với scope `function`
  - Test fixture hoạt động đúng

- [ ] **Bài 5.2 — Convert tests sang pytest style**
  - Lấy test file từ Tuần 4
  - Xóa code setup/teardown trong từng test
  - Dùng fixture thay thế

- [ ] **Bài 5.3 — Fixture nâng cao**
  ```python
  # Tạo các fixtures:
  # 1. driver (scope=function) - basic driver
  # 2. driver_module (scope=module) - dùng chung cho cả module
  # 3. login_page - fixture trả về LoginPage đã open
  # 4. authenticated_driver - driver đã login sẵn
  ```

- [ ] **Bài 5.4 — Parametrize với login scenarios**
  ```python
  # Viết 1 test function nhưng test được tất cả scenarios:
  # Valid login, invalid password, invalid username, empty fields
  # Dùng @pytest.mark.parametrize
  ```

- [ ] **Bài 5.5 — External test data**
  - Tạo file `test_data/login_data.json`:
    ```json
    [
      {"username": "tomsmith", "password": "SuperSecretPassword!", "should_pass": true},
      {"username": "wrong", "password": "wrong", "should_pass": false}
    ]
    ```
  - Load data từ JSON trong parametrize
  - Chạy và verify kết quả

- [ ] **Bài 5.6 — Marks và screenshot**
  - Đánh mark `@pytest.mark.smoke` cho 3 test quan trọng nhất
  - Đánh mark `@pytest.mark.regression` cho tất cả tests
  - Implement auto-screenshot khi test fail
  - Chạy `pytest -m smoke` và verify chỉ chạy 3 test

- [ ] **Bài 5.7 — conftest.py hoàn chỉnh**
  ```python
  # Tạo conftest.py với:
  # 1. driver fixture
  # 2. Screenshot on failure
  # 3. Fixture đọc config từ file
  # 4. Command line option để chọn browser:
  #    pytest --browser=firefox
  ```
  ```python
  # Gợi ý cho command line option:
  def pytest_addoption(parser):
      parser.addoption("--browser", default="chrome")
  
  @pytest.fixture
  def browser(request):
      return request.config.getoption("--browser")
  ```

### 🎯 Tiêu chí hoàn thành Tuần 5

- [ ] ✅ Giải thích được sự khác nhau giữa 4 fixture scopes
- [ ] ✅ `conftest.py` có driver fixture với yield (setup/teardown)
- [ ] ✅ Ít nhất 1 test dùng `@pytest.mark.parametrize` với 4+ data points
- [ ] ✅ Screenshot tự động khi test fail
- [ ] ✅ Chạy được `pytest -m smoke` chỉ chạy smoke tests
- [ ] ✅ `pytest -v` hiển thị tất cả test names rõ ràng

---

## ✅ TUẦN 6 — HTML Report, Parallel & CI/CD

> **Mục tiêu:** Tạo báo cáo đẹp, chạy test song song, và tích hợp vào CI/CD pipeline.

### 📚 Kiến thức cần học

#### 📊 HTML Reports

- [ ] **pytest-html:** Report đơn giản, tích hợp sẵn
  ```bash
  pip install pytest-html
  
  # Tạo report
  pytest --html=reports/report.html --self-contained-html
  ```

- [ ] **Allure Report:** Report chuyên nghiệp, đẹp hơn
  ```bash
  pip install allure-pytest
  
  # Chạy test và generate data
  pytest --alluredir=allure_results
  
  # Mở report (cần cài Allure CLI)
  allure serve allure_results
  ```

- [ ] **Thêm metadata vào Allure:**
  ```python
  import allure
  
  @allure.title("Đăng nhập với tài khoản hợp lệ")
  @allure.description("Test case kiểm tra luồng đăng nhập thành công")
  @allure.severity(allure.severity_level.CRITICAL)
  @allure.tag("smoke", "login")
  def test_valid_login(driver):
      with allure.step("Mở trang đăng nhập"):
          driver.get("https://the-internet.herokuapp.com/login")
      
      with allure.step("Nhập thông tin đăng nhập"):
          # ... login logic
          pass
      
      with allure.step("Verify đăng nhập thành công"):
          # ... assertion
          pass
  ```

#### ⚡ Chạy Parallel với pytest-xdist

- [ ] **Cài đặt:**
  ```bash
  pip install pytest-xdist
  ```

- [ ] **Chạy parallel:**
  ```bash
  # Chạy với 4 workers
  pytest -n 4
  
  # Chạy với số worker = số CPU
  pytest -n auto
  
  # Chạy 2 workers
  pytest -n 2 tests/
  ```

- [ ] **Lưu ý quan trọng khi chạy parallel:**
  - Mỗi worker cần browser riêng
  - Không dùng `scope=session` cho driver fixture (gây conflict)
  - Tránh test cases phụ thuộc vào nhau
  - Tránh dùng file/folder chung (screenshots cần unique name)

  ```python
  # Fix unique screenshot names khi parallel:
  import uuid
  filename = f"screenshots/FAILED_{test_name}_{uuid.uuid4()}.png"
  ```

#### 🔧 CI/CD với GitHub Actions

- [ ] **Khái niệm CI/CD:**
  - CI = Continuous Integration: tự động chạy test khi có code mới
  - CD = Continuous Delivery/Deployment: tự động deploy
  - Pipeline: chuỗi các bước tự động

- [ ] **Tạo GitHub Actions workflow:**
  ```yaml
  # .github/workflows/selenium_tests.yml
  name: Selenium Tests
  
  on:
    push:
      branches: [ main, develop ]
    pull_request:
      branches: [ main ]
    schedule:
      - cron: '0 8 * * *'  # Chạy mỗi ngày lúc 8 giờ sáng
  
  jobs:
    test:
      runs-on: ubuntu-latest
      
      steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
      
      - name: Setup Chrome
        uses: browser-actions/setup-chrome@v1
      
      - name: Run tests
        run: |
          pytest tests/ \
            --html=reports/report.html \
            --self-contained-html \
            -v \
            -n auto
      
      - name: Upload test report
        uses: actions/upload-artifact@v3
        if: always()  # Upload dù test pass hay fail
        with:
          name: test-report
          path: reports/report.html
      
      - name: Upload screenshots on failure
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: failure-screenshots
          path: screenshots/
  ```

- [ ] **Chrome trong headless mode cho CI:**
  ```python
  # conftest.py - tự động chọn headless khi chạy trên CI
  import os
  
  @pytest.fixture
  def driver():
      options = webdriver.ChromeOptions()
      
      # Chạy headless trên CI environment
      if os.environ.get('CI'):
          options.add_argument('--headless')
          options.add_argument('--no-sandbox')
          options.add_argument('--disable-dev-shm-usage')
          options.add_argument('--disable-gpu')
      
      service = Service(ChromeDriverManager().install())
      driver = webdriver.Chrome(service=service, options=options)
      driver.maximize_window()
      yield driver
      driver.quit()
  ```

#### 📄 requirements.txt hoàn chỉnh

```
# requirements.txt
selenium==4.15.2
webdriver-manager==4.0.1
pytest==7.4.3
pytest-html==4.1.1
pytest-xdist==3.3.1
allure-pytest==2.13.2
```

### 🏋️ Bài tập thực hành

- [ ] **Bài 6.1 — Tạo pytest-html report**
  ```bash
  # Chạy tất cả test và tạo report
  pytest tests/ --html=reports/report.html --self-contained-html -v
  
  # Mở file report.html trong browser
  # Chụp screenshot report để lưu lại
  ```

- [ ] **Bài 6.2 — Customize HTML report**
  ```python
  # conftest.py - thêm metadata vào report
  def pytest_configure(config):
      config._metadata = {
          "Project": "Selenium Learning Project",
          "Tester": "Tên của bạn",
          "Environment": "Test",
          "Browser": "Chrome"
      }
  ```

- [ ] **Bài 6.3 — Cài và thử Allure** (Bonus)
  - Cài Allure CLI
  - Thêm `@allure.title` và `@allure.step` vào ít nhất 3 test
  - Generate và xem Allure report

- [ ] **Bài 6.4 — Chạy parallel đầu tiên**
  ```bash
  # Đảm bảo có ít nhất 4 test cases
  
  # Chạy tuần tự và đo thời gian
  time pytest tests/ -v
  
  # Chạy parallel và đo thời gian
  time pytest tests/ -n 4 -v
  
  # So sánh thời gian
  ```

- [ ] **Bài 6.5 — Fix parallel issues**
  - Chạy `pytest -n 4` và tìm xem có test nào fail không
  - Nếu có: debug và fix nguyên nhân
  - Common issues: shared state, screenshot naming conflict

- [ ] **Bài 6.6 — Setup GitHub Actions**
  - Tạo GitHub repository
  - Push code lên GitHub
  - Tạo file `.github/workflows/selenium_tests.yml`
  - Commit và push → xem pipeline chạy trên GitHub Actions
  - Download report từ Artifacts

- [ ] **Bài 6.7 — Project cuối khóa 🏆**
  ```
  Trang test: https://www.saucedemo.com/
  
  Yêu cầu:
  ✅ Cấu trúc POM đầy đủ (ít nhất 3 Page Objects)
  ✅ Ít nhất 10 test cases cover các scenario:
     - Login/Logout
     - Thêm sản phẩm vào cart
     - Xóa sản phẩm khỏi cart
     - Checkout process
     - Sort sản phẩm
  ✅ Parametrize cho ít nhất 2 test
  ✅ Marks: smoke (3 test), regression (tất cả)
  ✅ Screenshot on failure
  ✅ HTML report
  ✅ Chạy được với pytest -n 2 (parallel)
  ✅ GitHub Actions pipeline
  ✅ README.md hướng dẫn cách chạy
  ```

### 🎯 Tiêu chí hoàn thành Tuần 6

- [ ] ✅ Generate được HTML report sau khi chạy test
- [ ] ✅ Chạy `pytest -n 4` thành công không có conflict
- [ ] ✅ Có file `.github/workflows/selenium_tests.yml`
- [ ] ✅ Pipeline GitHub Actions chạy xanh (hoặc fail đúng chỗ)
- [ ] ✅ Hoàn thành Project cuối khóa với đủ yêu cầu
- [ ] ✅ Có thể giải thích toàn bộ cấu trúc project cho người khác nghe

---

## 🏆 Tổng kết 6 tuần — Self Assessment

> Sau khi hoàn thành 6 tuần, tự đánh giá bản thân theo bảng sau:

| Kỹ năng | Chưa biết | Biết cơ bản | Thành thạo |
|---------|-----------|------------|-----------|
| Cài đặt môi trường | ⬜ | ⬜ | ⬜ |
| Dùng các loại Locator | ⬜ | ⬜ | ⬜ |
| Explicit Wait | ⬜ | ⬜ | ⬜ |
| Xử lý Alert/iFrame | ⬜ | ⬜ | ⬜ |
| Page Object Model | ⬜ | ⬜ | ⬜ |
| pytest Fixtures | ⬜ | ⬜ | ⬜ |
| Parametrize | ⬜ | ⬜ | ⬜ |
| HTML Report | ⬜ | ⬜ | ⬜ |
| Parallel Testing | ⬜ | ⬜ | ⬜ |
| GitHub Actions CI/CD | ⬜ | ⬜ | ⬜ |

### 📚 Tài nguyên học thêm

- 📖 [Selenium Official Docs](https://www.selenium.dev/documentation/)
- 📖 [pytest Documentation](https://docs.pytest.org/)
- 🎮 [Trang luyện tập: The Internet](https://the-internet.herokuapp.com/)
- 🎮 [Trang luyện tập: Sauce Demo](https://www.saucedemo.com/)
- 🎮 [Trang luyện tập: Selenium Demo](https://www.selenium.dev/selenium/web/web-form.html)
- 📺 [Selenium Python Tutorial - YouTube]

### ➡️ Bước tiếp theo sau 6 tuần

- [ ] Học **Selenium Grid** — chạy test trên nhiều máy/browser cùng lúc
- [ ] Học **API Testing** với `requests` library
- [ ] Học **Playwright** — automation tool thế hệ mới
- [ ] Đóng góp vào dự án thật — xin vào team QA automation
- [ ] Học **Docker** để container hóa test environment

---

> 💪 **Lời khuyên cuối:** Đừng copy-paste code — gõ tay từng dòng để ghi nhớ. Khi gặp lỗi, đừng panic — đọc kỹ error message, Google, và thử lại. Consistency quan trọng hơn speed!