NỘI DUNG BÀI HỌC
✅ Thiết lập class Keyword lưu trữ các hàm xử lý chung
✅ Tích hợp cơ chế waits vào các hàm xử lý chung
✅ Sử dụng class BasePage
Nên class BasePage chỉ chứa các element chung xuất hiện trên nhiều page như (menu, logout, error, loading...) và các hàm xử lý chung liên quan trực tiếp đến elements chung.
⭐️BasePage
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
public class BasePage {
//Constructor bắt buộc để init elements
public BasePage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
//Element/Locators thuộc chung cho nhiều trang
@AndroidFindBy(accessibility = "Date")
@iOSXCUITFindBy(accessibility = "Date")
public WebElement menuDate;
@AndroidFindBy(accessibility = "Menu")
@iOSXCUITFindBy(accessibility = "Menu")
public WebElement menuMenu;
@AndroidFindBy(accessibility = "Wallet")
@iOSXCUITFindBy(accessibility = "Wallet")
public WebElement menuWallet;
@AndroidFindBy(accessibility = "Profile")
@iOSXCUITFindBy(accessibility = "Profile")
public WebElement menuProfile;
@AndroidFindBy(accessibility = "Config")
@iOSXCUITFindBy(accessibility = "Config")
public WebElement menuConfig;
@AndroidFindBy(accessibility = "Open navigation menu")
@iOSXCUITFindBy(accessibility = "Open navigation menu")
public WebElement openNavigationLeftMenu;
@AndroidFindBy(accessibility = "Web view")
@iOSXCUITFindBy(accessibility = "Web view")
public WebElement itemWebView;
@AndroidFindBy(accessibility = "Back")
@iOSXCUITFindBy(accessibility = "Back")
public WebElement buttonBack;
//Các hàm xử lý chung cho nhiều trang đều có
public void clickMenuDate() {
menuDate.click();
}
public MenuPage clickMenuMenu() {
menuMenu.click();
return new MenuPage();
}
public void clickMenuWallet() {
menuWallet.click();
}
public ProfilePage clickMenuProfile() {
menuProfile.click();
return new ProfilePage();
}
public ConfigPage clickMenuConfig() {
menuConfig.click();
return new ConfigPage();
}
public void clickOpenNavigationLeftMenu() {
openNavigationLeftMenu.click();
}
public void clickItemWebView() {
itemWebView.click();
}
public void clickButtonBack() {
buttonBack.click();
}
}
Tất cả các class page LoginPage, MenuPage, ConfigPage, ProductPage, TablePage,...sẽ kế thừa lại class BasePage trên. Để có thể gọi sử dụng trực tiếp các Elements chung và Hàm xử lý chung.
⭐️LoginPage
Kế thừa BasePage và sử dụng lại element menuMenu.
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.testng.Assert;
public class LoginPage extends BasePage {
// Constructor
public LoginPage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
//Element/Locators thuộc chính trang này (màn hình này)
@AndroidFindBy(xpath = "//android.view.View[@content-desc=\"Mobile App Flutter Beta\"]/following-sibling::android.widget.EditText[1]")
@iOSXCUITFindBy(accessibility = "username")
private WebElement usernameField;
@AndroidFindBy(xpath = "//android.view.View[@content-desc=\"Mobile App Flutter Beta\"]/following-sibling::android.widget.EditText[2]")
@iOSXCUITFindBy(accessibility = "password")
private WebElement passwordField;
@AndroidFindBy(xpath = "//android.widget.Button[@content-desc=\"Sign in\"]")
@iOSXCUITFindBy(id = "loginBtn")
private WebElement loginButton;
@AndroidFindBy(accessibility = " Invalid email or password")
@iOSXCUITFindBy(accessibility = " Invalid email or password")
private WebElement errorMessage;
//Các hàm xử lý trong chính nội bộ trang này (màn hình này)
public MenuPage login(String username, String password) {
usernameField.click();
usernameField.sendKeys(username);
passwordField.click();
passwordField.sendKeys(password);
loginButton.click();
return new MenuPage();
}
public MenuPage verifyLoginSuccess() {
Assert.assertTrue(menuMenu.isDisplayed(), "The Table page not display. (Menu not found)");
return new MenuPage();
}
public void verifyLoginFail() {
Assert.assertTrue(errorMessage.isDisplayed(), "The error message not display.");
System.out.println(errorMessage.getAttribute("content-desc"));
Assert.assertEquals(errorMessage.getAttribute("content-desc"), " Invalid email or password", "The content of error message not display.");
}
}
⭐️MenuPage
Kế thừa BasePage và sử dụng lại hàm clickMenuMenu().
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.testng.Assert;
import java.util.List;
public class MenuPage extends BasePage {
// Constructor
public MenuPage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
//Element/Locators thuộc chính trang này (màn hình này)
@AndroidFindBy(xpath = "//android.widget.EditText")
@iOSXCUITFindBy(accessibility = "")
private WebElement inputSearch;
@AndroidFindBy(xpath = "(//android.view.View[contains(@content-desc,\"Table\")])[1]")
@iOSXCUITFindBy(accessibility = "")
private WebElement firstItemTable;
@AndroidFindBy(xpath = "//android.view.View[contains(@content-desc,\"Table\")]")
@iOSXCUITFindBy(xpath = "")
private List<WebElement> listItemTable;
public void searchTable(String tableName) {
clickMenuMenu();
inputSearch.click();
inputSearch.sendKeys(tableName);
}
public void checkTableResultTotal(int expectedTotal) {
List<WebElement> listTables = listItemTable;
System.out.println("Table total: " + listTables.size());
Assert.assertTrue(listTables.size() >= expectedTotal);
}
}
⭐️ConfigPage
Kế thừa BasePage. Có các hàm liên kết trang đến ProductPage, TablePage, ServerPage và LoginPage.
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
public class ConfigPage extends BasePage {
public ConfigPage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
@AndroidFindBy(xpath = "//android.widget.Button[contains(@content-desc,\"Product management\")]")
@iOSXCUITFindBy(xpath = "")
private WebElement itemProductManagement;
@AndroidFindBy(accessibility = "Server database")
@iOSXCUITFindBy(accessibility = "Server database")
private WebElement itemServerDatabase;
@AndroidFindBy(xpath = "//android.widget.Button[contains(@content-desc,\"Tables management\")]")
@iOSXCUITFindBy(xpath = "")
private WebElement itemTableManagement;
@AndroidFindBy(accessibility = "Logout")
@iOSXCUITFindBy(accessibility = "Logout")
private WebElement itemLogout;
public ProductPage openProductManagement() {
itemProductManagement.click();
return new ProductPage();
}
public ServerPage openServerDatabase() {
itemServerDatabase.click();
return new ServerPage();
}
public TablePage openTableManagement() {
itemTableManagement.click();
return new TablePage();
}
public LoginPage logout() {
itemLogout.click();
return new LoginPage();
}
}
⭐️ProductPage
Kế thừa BasePage. Khai báo các hàm trong trang Product management.
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
public class ProductPage extends BasePage {
public ProductPage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
@AndroidFindBy(accessibility = "Add Product")
@iOSXCUITFindBy(accessibility = "Add Product")
private WebElement buttonAddNewProduct;
@AndroidFindBy(xpath = "//android.widget.EditText")
@iOSXCUITFindBy(xpath = "")
private WebElement inputSearchProduct;
public void addNewProduct() {
System.out.println("Add new product");
buttonAddNewProduct.click();
//Viết tiếp
}
}
⭐️TablePage
Kế thừa BasePage. Khai báo các hàm trong trang Table management.
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
public class TablePage extends BasePage {
public TablePage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
@AndroidFindBy(accessibility = "Add Table")
@iOSXCUITFindBy(accessibility = "Add Table")
private WebElement buttonAddNewTable;
@AndroidFindBy(xpath = "//android.widget.EditText")
@iOSXCUITFindBy(xpath = "")
private WebElement inputSearchTable;
public void addNewTable() {
System.out.println("Add new table");
buttonAddNewTable.click();
//Viết tiếp
}
}
✅ Thiết lập class Keyword lưu trữ các hàm xử lý chung
Việc thiết lập một class Keyword để lưu trữ các hàm xử lý chung trong automation testing (như với Appium, Selenium, hoặc các framework khác) mang nhiều ý nghĩa quan trọng, đặc biệt trong việc tổ chức code và tối ưu hóa quy trình kiểm thử.Dưới đây là ý nghĩa chi tiết:
1. Tái sử dụng code (Reusability)
- Class Keyword chứa các hàm xử lý chung (ví dụ: click, sendKeys, wait, verify...) mà nhiều test case hoặc page object có thể sử dụng. Thay vì viết lại logic xử lý ở mỗi class, bạn chỉ cần gọi các hàm từ Keyword, giảm thiểu sự lặp lại code.
- Ví dụ: Một hàm clickElement(WebElement element) có thể được dùng ở bất kỳ page nào mà không cần định nghĩa lại.
- Khi logic xử lý được tập trung trong class Keyword, nếu cần thay đổi cách xử lý (ví dụ: thêm logging, thay đổi thời gian wait), bạn chỉ cần chỉnh sửa ở một nơi duy nhất thay vì phải sửa ở nhiều class khác.
- Điều này đặc biệt hữu ích khi dự án lớn lên hoặc yêu cầu thay đổi.
- Các hàm trong Keyword đảm bảo rằng mọi hành động (như click, nhập text, kiểm tra element) được thực hiện theo cùng một cách trên toàn bộ hệ thống. Ví dụ: Mọi click đều có thể bao gồm wait trước khi thực hiện, tránh lỗi không đồng bộ.
- Class Keyword tách biệt logic kỹ thuật (cách tương tác với element) khỏi kịch bản kiểm thử (test case). Điều này giúp người viết test case tập trung vào luồng nghiệp vụ thay vì chi tiết kỹ thuật.
- Ví dụ: Thay vì viết wait.until(...).click(), người dùng chỉ cần gọi Keyword.clickElement(element).
- Nếu bạn áp dụng mô hình Keyword-Driven Testing, class Keyword là nơi định nghĩa các "từ khóa" (hành động) như "click", "input", "verify". Điều này cho phép tạo test case từ file dữ liệu (Excel, JSON) mà không cần viết code phức tạp.
- Ví dụ: Trong file Excel, bạn có thể định nghĩa: Click | loginButton và Keyword sẽ xử lý.
- Khi hệ thống phát triển, bạn có thể dễ dàng thêm các hàm xử lý mới vào class Keyword mà không ảnh hưởng đến các class khác. Ví dụ: Thêm hàm swipeToElement() khi cần test gesture trên mobile.
Chúng ta đã và đang tạo class MobileUI để khai báo các hàm đã học ở các bài trước với trạng thái là các hàm static chỉ cần lấy tên class MobileUI chấm gọi tên hàm là có thể sử dụng, không cần khởi tạo object class.
Giờ chúng ta định nghĩa lại class MobileUI cho đầy đủ hơn:
⭐️MobileUI
package com.anhtester.keywords;
import com.anhtester.drivers.DriverManager;
import org.openqa.selenium.*;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.interactions.Pause;
import org.openqa.selenium.interactions.PointerInput;
import org.openqa.selenium.interactions.Sequence;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import java.time.Duration;
import java.util.*;
import static com.anhtester.drivers.DriverManager.getDriver;
public class MobileUI {
private static final int DEFAULT_TIMEOUT = 10;
public static void sleep(double second) {
System.out.println("[MobileUI] Sleeping for " + second + " seconds.");
try {
Thread.sleep((long) (1000 * second));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void swipe(int startX, int startY, int endX, int endY, int durationMillis) {
System.out.println("[MobileUI] Executing swipe from (" + startX + "," + startY + ") to (" + endX + "," + endY + ") with duration " + durationMillis + "ms.");
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence swipe = new Sequence(finger, 1);
swipe.addAction(finger.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), startX, startY));
swipe.addAction(finger.createPointerDown(0));
swipe.addAction(finger.createPointerMove(Duration.ofMillis(durationMillis), PointerInput.Origin.viewport(), endX, endY));
swipe.addAction(finger.createPointerUp(0));
getDriver().perform(Collections.singletonList(swipe));
}
public static void swipeLeft() {
System.out.println("[MobileUI] Executing swipeLeft.");
Dimension size = getDriver().manage().window().getSize();
int startX = (int) (size.width * 0.8);
int startY = (int) (size.height * 0.3);
int endX = (int) (size.width * 0.2);
int endY = startY;
int duration = 200;
swipe(startX, startY, endX, endY, duration);
}
public static void swipeRight() {
System.out.println("[MobileUI] Executing swipeRight.");
Dimension size = getDriver().manage().window().getSize();
int startX = (int) (size.width * 0.2);
int startY = (int) (size.height * 0.3);
int endX = (int) (size.width * 0.8);
int endY = startY;
int duration = 200;
swipe(startX, startY, endX, endY, duration);
}
private static Point getCenterOfElement(Point location, Dimension size) {
// No log needed for private helper, logging happens in the calling public method
return new Point(location.getX() + size.getWidth() / 2,
location.getY() + size.getHeight() / 2);
}
public static void tap(WebElement element) {
System.out.println("[MobileUI] Executing tap on element: " + element);
Point location = element.getLocation();
Dimension size = element.getSize();
Point centerOfElement = getCenterOfElement(location, size);
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence sequence = new Sequence(finger, 1)
.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), centerOfElement))
.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
.addAction(new Pause(finger, Duration.ofMillis(500))) // Note: Default pause is 500ms here
.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
getDriver().perform(Collections.singletonList(sequence));
}
public static void tap(int x, int y) {
System.out.println("[MobileUI] Executing tap at coordinates (" + x + "," + y + ") with 200ms pause.");
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence tap = new Sequence(finger, 1);
tap.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), x, y));
tap.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
tap.addAction(new Pause(finger, Duration.ofMillis(200))); //Chạm nhẹ nhanh
tap.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
getDriver().perform(Arrays.asList(tap));
}
public static void tap(int x, int y, int milliSecondDuration) {
System.out.println("[MobileUI] Executing tap at coordinates (" + x + "," + y + ") with pause " + milliSecondDuration + "ms.");
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence tap = new Sequence(finger, 1);
tap.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), x, y));
tap.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
tap.addAction(new Pause(finger, Duration.ofMillis(milliSecondDuration))); //Chạm vào với thời gian chỉ định
tap.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
getDriver().perform(Arrays.asList(tap));
}
public static void zoom(WebElement element, double scale) {
System.out.println("[MobileUI] Executing zoom on element: " + element + " with approximate scale factor: " + scale + " (Note: Implementation may need review for accurate scaling)");
int centerX = element.getLocation().getX() + element.getSize().getWidth() / 2;
int centerY = element.getLocation().getY() + element.getSize().getHeight() / 2;
int distance = 100; // Khoảng cách giữa hai ngón tay
PointerInput finger1 = new PointerInput(PointerInput.Kind.TOUCH, "finger1");
PointerInput finger2 = new PointerInput(PointerInput.Kind.TOUCH, "finger2");
Sequence zoom = new Sequence(finger1, 1);
zoom.addAction(finger1.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), centerX - distance, centerY));
zoom.addAction(finger1.createPointerDown(0));
Sequence zoom2 = new Sequence(finger2, 1);
zoom2.addAction(finger2.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), centerX + distance, centerY));
zoom2.addAction(finger2.createPointerDown(0));
// Simplified movement - Actual scaling might need more complex radial movement logic
int moveDuration = 50;
int steps = 10;
int startDist1X = centerX - distance;
int startDist2X = centerX + distance;
int endDist1X, endDist2X;
if (scale > 1) { // Phóng to - Move fingers further apart
System.out.println("[MobileUI] Zooming In");
endDist1X = centerX - (int) (distance * scale); // Example: move further left
endDist2X = centerX + (int) (distance * scale); // Example: move further right
} else { // Thu nhỏ - Move fingers closer
System.out.println("[MobileUI] Zooming Out");
endDist1X = centerX - (int) (distance * scale); // Example: move closer to center
endDist2X = centerX + (int) (distance * scale); // Example: move closer to center
}
for (int i = 1; i <= steps; i++) {
int currentX1 = startDist1X + (endDist1X - startDist1X) * i / steps;
int currentX2 = startDist2X + (endDist2X - startDist2X) * i / steps;
zoom.addAction(finger1.createPointerMove(Duration.ofMillis(moveDuration), PointerInput.Origin.viewport(), currentX1, centerY));
zoom2.addAction(finger2.createPointerMove(Duration.ofMillis(moveDuration), PointerInput.Origin.viewport(), currentX2, centerY));
}
zoom.addAction(finger1.createPointerUp(0));
zoom2.addAction(finger2.createPointerUp(0));
getDriver().perform(Arrays.asList(zoom, zoom2));
}
public static void scroll(int startX, int startY, int endX, int endY, int durationMillis) {
System.out.println("[MobileUI] Executing scroll from (" + startX + "," + startY + ") to (" + endX + "," + endY + ") with duration " + durationMillis + "ms.");
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence swipe = new Sequence(finger, 1);
swipe.addAction(finger.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.viewport(), startX, startY));
swipe.addAction(finger.createPointerDown(0));
swipe.addAction(finger.createPointerMove(Duration.ofMillis(durationMillis), PointerInput.Origin.viewport(), endX, endY));
swipe.addAction(finger.createPointerUp(0));
getDriver().perform(Collections.singletonList(swipe));
}
public static void scrollGestureCommand() {
// Scroll gesture cho Android
Map<String, Object> scrollParams = new HashMap<>();
scrollParams.put("left", 670);
scrollParams.put("top", 500);
scrollParams.put("width", 200);
scrollParams.put("height", 2000);
scrollParams.put("direction", "down");
scrollParams.put("percent", 1);
System.out.println("[MobileUI] Executing scrollGesture command with params: " + scrollParams);
// Thực hiện scroll gesture
getDriver().executeScript("mobile: scrollGesture", scrollParams);
}
public static void clickElement(By locator, int second) {
System.out.println("[MobileUI] Clicking element located by: " + locator + " within " + second + "s.");
waitForElementToBeClickable(locator, second).click();
}
public static void clickElement(By locator) {
System.out.println("[MobileUI] Clicking element located by: " + locator + " within default timeout (" + DEFAULT_TIMEOUT + "s).");
waitForElementToBeClickable(locator).click();
}
public static void clickElement(WebElement element, int second) {
System.out.println("[MobileUI] Clicking element: " + element + " within " + second + "s.");
waitForElementToBeClickable(element, second).click();
}
public static void clickElement(WebElement element) {
System.out.println("[MobileUI] Clicking element: " + element + " within default timeout (" + DEFAULT_TIMEOUT + "s).");
waitForElementToBeClickable(element).click();
}
public static void setText(By locator, String text) {
System.out.println("[MobileUI] Setting text '" + text + "' on element located by: " + locator + " with default timeout.");
WebElement element = waitForElementVisibe(locator);
element.click(); // Often needed before clear/sendKeys
element.clear();
element.sendKeys(text);
System.out.println("[MobileUI] Set text completed for locator: " + locator);
}
public static void setText(By locator, String text, int second) {
System.out.println("[MobileUI] Setting text '" + text + "' on element located by: " + locator + " with timeout " + second + "s.");
WebElement element = waitForElementVisibe(locator, second);
element.click();
element.clear();
element.sendKeys(text);
System.out.println("[MobileUI] Set text completed for locator: " + locator);
}
public static void setText(WebElement element, String text) {
System.out.println("[MobileUI] Setting text '" + text + "' on element: " + element + " with default timeout.");
WebElement elm = waitForElementVisibe(element);
elm.click();
elm.clear();
elm.sendKeys(text);
System.out.println("[MobileUI] Set text completed for element: " + element);
}
public static void setText(WebElement element, String text, int second) {
System.out.println("[MobileUI] Setting text '" + text + "' on element: " + element + " with timeout " + second + "s.");
WebElement elm = waitForElementVisibe(element, second);
elm.click();
elm.clear();
elm.sendKeys(text);
System.out.println("[MobileUI] Set text completed for element: " + element);
}
public static void clearText(By locator) {
System.out.println("[MobileUI] Clearing text on element located by: " + locator + " with default timeout.");
WebElement element = waitForElementVisibe(locator);
element.click();
element.clear();
System.out.println("[MobileUI] Clear text completed for locator: " + locator);
}
public static void clearText(By locator, int second) {
System.out.println("[MobileUI] Clearing text on element located by: " + locator + " with timeout " + second + "s.");
WebElement element = waitForElementVisibe(locator, second);
element.click();
element.clear();
System.out.println("[MobileUI] Clear text completed for locator: " + locator);
}
public static void clearText(WebElement element) {
System.out.println("[MobileUI] Clearing text on element: " + element + " with default timeout.");
WebElement elm = waitForElementVisibe(element);
elm.click();
elm.clear();
System.out.println("[MobileUI] Clear text completed for element: " + element);
}
public static void clearText(WebElement element, int second) {
System.out.println("[MobileUI] Clearing text on element: " + element + " with timeout " + second + "s.");
WebElement elm = waitForElementVisibe(element, second);
elm.click();
elm.clear();
System.out.println("[MobileUI] Clear text completed for element: " + element);
}
public static String getElementText(By locator) {
System.out.println("[MobileUI] Getting text from element located by: " + locator + " with default timeout.");
WebElement element = waitForElementVisibe(locator);
String text = element.getText();
System.out.println("[MobileUI] Retrieved text: '" + text + "'");
return text;
}
public static String getElementText(By locator, int second) {
System.out.println("[MobileUI] Getting text from element located by: " + locator + " with timeout " + second + "s.");
WebElement element = waitForElementVisibe(locator, second);
String text = element.getText();
System.out.println("[MobileUI] Retrieved text: '" + text + "'");
return text;
}
public static String getElementText(WebElement element) {
System.out.println("[MobileUI] Getting text from element: " + element + " with default timeout.");
WebElement elm = waitForElementVisibe(element);
String text = elm.getText();
System.out.println("[MobileUI] Retrieved text: '" + text + "'");
return text;
}
public static String getElementText(WebElement element, int second) {
System.out.println("[MobileUI] Getting text from element: " + element + " with timeout " + second + "s.");
WebElement elm = waitForElementVisibe(element, second);
String text = elm.getText();
System.out.println("[MobileUI] Retrieved text: '" + text + "'");
return text;
}
public static String getElementAttribute(By locator, String attribute) {
System.out.println("[MobileUI] Getting attribute '" + attribute + "' from element located by: " + locator + " with default timeout.");
WebElement element = waitForElementVisibe(locator);
String value = element.getAttribute(attribute);
System.out.println("[MobileUI] Retrieved attribute value: '" + value + "'");
return value;
}
public static String getElementAttribute(By locator, String attribute, int second) {
System.out.println("[MobileUI] Getting attribute '" + attribute + "' from element located by: " + locator + " with timeout " + second + "s.");
WebElement element = waitForElementVisibe(locator, second);
String value = element.getAttribute(attribute);
System.out.println("[MobileUI] Retrieved attribute value: '" + value + "'");
return value;
}
public static String getElementAttribute(WebElement element, String attribute) {
System.out.println("[MobileUI] Getting attribute '" + attribute + "' from element: " + element + " with default timeout.");
WebElement elm = waitForElementVisibe(element);
String value = elm.getAttribute(attribute);
System.out.println("[MobileUI] Retrieved attribute value: '" + value + "'");
return value;
}
public static String getElementAttribute(WebElement element, String attribute, int second) {
System.out.println("[MobileUI] Getting attribute '" + attribute + "' from element: " + element + " with timeout " + second + "s.");
WebElement elm = waitForElementVisibe(element, second);
String value = elm.getAttribute(attribute);
System.out.println("[MobileUI] Retrieved attribute value: '" + value + "'");
return value;
}
public static boolean isElementPresentAndDisplayed(WebElement element) {
System.out.println("[MobileUI] Checking if element is present and displayed: " + element);
boolean result;
try {
result = element != null && element.isDisplayed();
System.out.println("[MobileUI] Element present and displayed check result: " + result);
return result;
} catch (NoSuchElementException e) {
System.out.println("[MobileUI] Element not found during presence/display check: " + element + " - " + e.getMessage());
return false;
} catch (Exception e) {
System.out.println("[MobileUI] An error occurred checking presence/display for element: " + element + " - " + e.getMessage());
return false;
}
}
public static boolean isElementPresentAndDisplayed(By locator) {
System.out.println("[MobileUI] Checking if element is present and displayed: " + locator);
boolean result;
try {
WebElement element = getDriver().findElement(locator); // Find first, then check display
result = element != null && element.isDisplayed();
System.out.println("[MobileUI] Element present and displayed check result: " + result + " for locator: " + locator);
return result;
} catch (NoSuchElementException e) {
System.out.println("[MobileUI] Element not found during presence/display check: " + locator + " - " + e.getMessage());
return false;
} catch (Exception e) {
System.out.println("[MobileUI] An error occurred checking presence/display for locator: " + locator + " - " + e.getMessage());
return false;
}
}
public static boolean isElementEnabled(WebElement element) {
System.out.println("[MobileUI] Checking if element is enabled: " + element);
boolean result;
try {
result = element != null && element.isEnabled();
System.out.println("[MobileUI] Element enabled check result: " + result);
return result;
} catch (Exception e) {
System.out.println("[MobileUI] An error occurred checking enabled status for element: " + element + " - " + e.getMessage());
return false;
}
}
public static boolean isElementEnabled(By locator) {
System.out.println("[MobileUI] Checking if element is enabled: " + locator);
boolean result;
try {
WebElement element = waitForElementVisibe(locator); // Ensure it's visible before checking enabled
result = element != null && element.isEnabled();
System.out.println("[MobileUI] Element enabled check result: " + result + " for locator: " + locator);
return result;
} catch (Exception e) {
System.out.println("[MobileUI] An error occurred checking enabled status for locator: " + locator + " - " + e.getMessage());
return false;
}
}
public static boolean isElementSelected(WebElement element) {
System.out.println("[MobileUI] Checking if element is selected: " + element);
boolean result;
try {
result = element != null && element.isSelected();
System.out.println("[MobileUI] Element selected check result: " + result);
return result;
} catch (Exception e) {
System.out.println("[MobileUI] An error occurred checking selected status for element: " + element + " - " + e.getMessage());
return false;
}
}
public static boolean isElementSelected(By locator) {
System.out.println("[MobileUI] Checking if element is selected: " + locator);
boolean result;
try {
WebElement element = waitForElementVisibe(locator); // Ensure it's visible before checking selected
result = element != null && element.isSelected();
System.out.println("[MobileUI] Element selected check result: " + result + " for locator: " + locator);
return result;
} catch (Exception e) {
System.out.println("[MobileUI] An error occurred checking selected status for locator: " + locator + " - " + e.getMessage());
return false;
}
}
// Các hàm verify (sử dụng Assert và gọi lại các hàm is)
public static void verifyElementPresentAndDisplayed(WebElement element, String message) {
System.out.println("[MobileUI] Verifying element is present and displayed: " + element + ". Message if failed: " + message);
Assert.assertTrue(isElementPresentAndDisplayed(element), message);
}
public static void verifyElementPresentAndDisplayed(By locator, String message) {
System.out.println("[MobileUI] Verifying element is present and displayed: " + locator + ". Message if failed: " + message);
Assert.assertTrue(isElementPresentAndDisplayed(locator), message);
}
public static void verifyElementEnabled(WebElement element, String message) {
System.out.println("[MobileUI] Verifying element is enabled: " + element + ". Message if failed: " + message);
Assert.assertTrue(isElementEnabled(element), message);
}
public static void verifyElementEnabled(By locator, String message) {
System.out.println("[MobileUI] Verifying element is enabled: " + locator + ". Message if failed: " + message);
Assert.assertTrue(isElementEnabled(locator), message);
}
public static void verifyElementSelected(WebElement element, String message) {
System.out.println("[MobileUI] Verifying element is selected: " + element + ". Message if failed: " + message);
Assert.assertTrue(isElementSelected(element), message);
}
public static void verifyElementSelected(By locator, String message) {
System.out.println("[MobileUI] Verifying element is selected: " + locator + ". Message if failed: " + message);
Assert.assertTrue(isElementSelected(locator), message);
}
public static void verifyElementText(WebElement element, String expectedText, String message) {
System.out.println("[MobileUI] Verifying text of element: " + element + " equals '" + expectedText + "'. Message if failed: " + message);
Assert.assertEquals(getElementText(element), expectedText, message);
}
public static void verifyElementText(By locator, String expectedText, String message) {
System.out.println("[MobileUI] Verifying text of element: " + locator + " equals '" + expectedText + "'. Message if failed: " + message);
Assert.assertEquals(getElementText(locator), expectedText, message);
}
public static void verifyElementAttribute(WebElement element, String attribute, String expectedValue, String message) {
System.out.println("[MobileUI] Verifying attribute '" + attribute + "' of element: " + element + " equals '" + expectedValue + "'. Message if failed: " + message);
Assert.assertEquals(getElementAttribute(element, attribute), expectedValue, message);
}
public static void verifyElementAttribute(By locator, String attribute, String expectedValue, String message) {
System.out.println("[MobileUI] Verifying attribute '" + attribute + "' of element: " + locator + " equals '" + expectedValue + "'. Message if failed: " + message);
Assert.assertEquals(getElementAttribute(locator, attribute), expectedValue, message);
}
public static void assertTrueCondition(boolean condition, String message) {
System.out.println("[MobileUI] Asserting condition: " + condition + ". Message if failed: " + message);
Assert.assertTrue(condition, message);
System.out.println("[MobileUI] Assertion passed for condition: " + condition);
}
// --- Wait Methods ---
public static WebElement waitForElementToBeClickable(By locator, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for element to be clickable: " + locator);
WebDriverWait wait = new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.elementToBeClickable(locator));
}
public static WebElement waitForElementToBeClickable(By locator) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for element to be clickable: " + locator);
WebDriverWait wait = new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.elementToBeClickable(locator));
}
public static WebElement waitForElementToBeClickable(WebElement element, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for element to be clickable: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.elementToBeClickable(element));
}
public static WebElement waitForElementToBeClickable(WebElement element) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for element to be clickable: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.elementToBeClickable(element));
}
public static WebElement waitForElementVisibe(By locator, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for element to be visible: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
public static WebElement waitForElementVisibe(By locator) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for element to be visible: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
public static WebElement waitForElementVisibe(WebElement element, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for element to be visible: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.visibilityOf(element));
}
public static WebElement waitForElementVisibe(WebElement element) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for element to be visible: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.visibilityOf(element));
}
public static boolean waitForElementInvisibe(By locator, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for element to be invisible: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.invisibilityOfElementLocated(locator));
}
public static boolean waitForElementInvisibe(By locator) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for element to be invisible: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.invisibilityOfElementLocated(locator));
}
public static boolean waitForElementInvisibe(WebElement element, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for element to be invisible: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.invisibilityOf(element));
}
public static boolean waitForElementInvisibe(WebElement element) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for element to be invisible: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.invisibilityOf(element));
}
public static WebElement waitForElementPresent(By locator, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for element to be present in DOM: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.presenceOfElementLocated(locator));
}
public static WebElement waitForElementPresent(By locator) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for element to be present in DOM: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.presenceOfElementLocated(locator));
}
public static boolean waitForTextToBePresent(By locator, String text, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for text '" + text + "' to be present in element: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
}
public static boolean waitForTextToBePresent(By locator, String text) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for text '" + text + "' to be present in element: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
}
public static boolean waitForTextToBePresent(WebElement element, String text, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for text '" + text + "' to be present in element: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.textToBePresentInElement(element, text));
}
public static boolean waitForTextToBePresent(WebElement element, String text) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for text '" + text + "' to be present in element: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.textToBePresentInElement(element, text));
}
public static boolean waitForAttributeToBe(By locator, String attribute, String value, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for attribute '" + attribute + "' to be '" + value + "' in element: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.attributeToBe(locator, attribute, value));
}
public static boolean waitForAttributeToBe(By locator, String attribute, String value) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for attribute '" + attribute + "' to be '" + value + "' in element: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.attributeToBe(locator, attribute, value));
}
public static boolean waitForAttributeToBe(WebElement element, String attribute, String value, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for attribute '" + attribute + "' to be '" + value + "' in element: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.attributeToBe(element, attribute, value));
}
public static boolean waitForAttributeToBe(WebElement element, String attribute, String value) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for attribute '" + attribute + "' to be '" + value + "' in element: " + element);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.attributeToBe(element, attribute, value));
}
public static List<WebElement> waitForNumberOfElements(By locator, int expectedCount, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for number of elements to be " + expectedCount + " for locator: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.numberOfElementsToBe(locator, expectedCount));
}
public static List<WebElement> waitForNumberOfElements(By locator, int expectedCount) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for number of elements to be " + expectedCount + " for locator: " + locator);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.numberOfElementsToBe(locator, expectedCount));
}
public static boolean waitForUrlContains(String text, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for URL to contain: '" + text + "'");
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.urlContains(text));
}
public static boolean waitForUrlContains(String text) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for URL to contain: '" + text + "'");
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.urlContains(text));
}
public static boolean waitForNumberOfWindows(int expectedWindows, int timeout) {
System.out.println("[MobileUI] Waiting up to " + timeout + "s for number of windows to be: " + expectedWindows);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeout));
return wait.until(ExpectedConditions.numberOfWindowsToBe(expectedWindows));
}
public static boolean waitForNumberOfWindows(int expectedWindows) {
System.out.println("[MobileUI] Waiting up to " + DEFAULT_TIMEOUT + "s (default) for number of windows to be: " + expectedWindows);
WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(DEFAULT_TIMEOUT));
return wait.until(ExpectedConditions.numberOfWindowsToBe(expectedWindows));
}
}
🔆 Đã tích hợp cơ chế waits vào các hàm xử lý chung trong MobileUI class luôn rồi.
🔆 Gọi lại các hàm chung sử dụng tại các class page xử lý
⭐️ LoginPage
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import com.anhtester.keywords.MobileUI;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
public class LoginPage extends BasePage {
// Constructor
public LoginPage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
//Element/Locators thuộc chính trang này (màn hình này)
@AndroidFindBy(xpath = "//android.view.View[@content-desc=\"Mobile App Flutter Beta\"]/following-sibling::android.widget.EditText[1]")
@iOSXCUITFindBy(accessibility = "username")
private WebElement usernameField;
@AndroidFindBy(xpath = "//android.view.View[@content-desc=\"Mobile App Flutter Beta\"]/following-sibling::android.widget.EditText[2]")
@iOSXCUITFindBy(accessibility = "password")
private WebElement passwordField;
@AndroidFindBy(xpath = "//android.widget.Button[@content-desc=\"Sign in\"]")
@iOSXCUITFindBy(id = "loginBtn")
private WebElement loginButton;
@AndroidFindBy(accessibility = " Invalid email or password")
@iOSXCUITFindBy(accessibility = " Invalid email or password")
private WebElement errorMessage;
//Các hàm xử lý trong chính nội bộ trang này (màn hình này)
public MenuPage login(String username, String password) {
MobileUI.clickElement(usernameField); // Click vào username field
MobileUI.setText(usernameField, username); // Nhập username
MobileUI.clickElement(passwordField); // Click vào password field
MobileUI.setText(passwordField, password); // Nhập password
MobileUI.clickElement(loginButton); // Click nút login
return new MenuPage();
}
public MenuPage verifyLoginSuccess() {
// Sử dụng MobileUI để verify
MobileUI.verifyElementPresentAndDisplayed(menuMenu, "The Table page not display. (Menu not found)");
return new MenuPage();
}
public void verifyLoginFail() {
// Sử dụng MobileUI để verify
MobileUI.verifyElementPresentAndDisplayed(errorMessage, "The error message not display.");
System.out.println(MobileUI.getElementAttribute(errorMessage, "content-desc"));
MobileUI.verifyElementAttribute(errorMessage, "content-desc", " Invalid email or password",
"The content of error message not display.");
}
}
⭐️ MenuPage
package com.anhtester.Bai19_HamXuLyChung.pages;
import com.anhtester.drivers.DriverManager;
import com.anhtester.keywords.MobileUI;
import io.appium.java_client.pagefactory.AndroidFindBy;
import io.appium.java_client.pagefactory.AppiumFieldDecorator;
import io.appium.java_client.pagefactory.iOSXCUITFindBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.testng.Assert;
import java.util.List;
public class MenuPage extends BasePage {
// Constructor
public MenuPage() {
PageFactory.initElements(new AppiumFieldDecorator(DriverManager.getDriver()), this);
}
//Element/Locators thuộc chính trang này (màn hình này)
@AndroidFindBy(xpath = "//android.widget.EditText")
@iOSXCUITFindBy(accessibility = "")
private WebElement inputSearch;
@AndroidFindBy(xpath = "(//android.view.View[contains(@content-desc,\"Table\")])[1]")
@iOSXCUITFindBy(accessibility = "")
private WebElement firstItemTable;
@AndroidFindBy(xpath = "//android.view.View[contains(@content-desc,\"Table\")]")
@iOSXCUITFindBy(xpath = "")
private List<WebElement> listItemTable;
public void searchTable(String tableName) {
clickMenuMenu();
MobileUI.clickElement(inputSearch); // Click vào ô tìm kiếm
MobileUI.setText(inputSearch, tableName); // Nhập từ khóa tìm kiếm
}
public void checkTableResultTotal(int expectedTotal) {
List<WebElement> listTables = listItemTable;
System.out.println("Table total: " + listTables.size());
Assert.assertTrue(listTables.size() >= expectedTotal);
}
}
🔥 Tại các class Test vẫn giữ nguyên KHÔNG chỉnh sửa gì hết nhé, vì chúng ta chỉ chỉnh sửa nội dung của các hàm xử lý trong class Page.