NỘI DUNG BÀI HỌC
📤 Xử lý upload (kể cả input ẩn) bằng setInputFiles và download bằng waitForEvent.
🖼️ Tương tác với iframe (ví dụ: cổng thanh toán, quảng cáo) bằng page.frameLocator().
🗄️ Tự động "xuyên" Shadow DOM (Open) để tìm element như bình thường.
🔒 Hiểu rõ giới hạn của Shadow DOM (Closed) và khi nào bắt buộc dùng evaluate().
🏞️ Phát hiện ảnh hỏng (404/CORS) bằng evaluate (check naturalWidth) và network response.
📤 Phần 1: Upload và Download File
Nhiều web hiện đại ẩn thẻ <input type="file"> thật và hiển thị một nút bấm đẹp thay thế. Nếu click vào nút "giả" đó, cửa sổ chọn file của hệ điều hành sẽ mở ra, mà Playwright không tương tác được, khiến test bị treo.
Cách đúng trong khoảng 95% trường hợp là dùng setInputFiles(). Phương thức này gán file trực tiếp vào input, bỏ qua kiểm tra actionability nên vẫn chạy được cả khi input bị ẩn.
// Gán 1 file, kể cả khi input đang bị ẩn
await page.locator("#real-input").setInputFiles("path/to/my-file.jpg");
// Upload nhiều file cùng lúc: truyền một mảng
await page.locator("#multi-input").setInputFiles([
"path/to/file1.jpg",
"path/to/file2.png",
]);
// Xóa các file đã chọn: truyền mảng rỗng
await page.locator("#multi-input").setInputFiles([]);
🔹 1. Upload file "ảo" bằng Buffer
Bạn không cần file thật trên ổ đĩa. Có thể tạo nội dung trong bộ nhớ rồi upload luôn, rất tiện cho test data động.
// Tạo file text "ảo" ngay trong bộ nhớ
const myBuffer = Buffer.from("Đây là nội dung file text ảo.");
await page.locator("#file-input").setInputFiles({
name: "ten_file.txt", // Tên file hiển thị
mimeType: "text/plain", // Loại file
buffer: myBuffer, // Nội dung file
});
// Hoặc chụp màn hình rồi upload luôn ảnh đó
const screenshot = await page.screenshot();
await page.locator("#avatar-input").setInputFiles({
name: "screenshot.png",
mimeType: "image/png",
buffer: screenshot,
});
🔹 2. Khi không có thẻ input trong DOM
Với khoảng 5% trường hợp còn lại, trang không hề có thẻ <input type="file">. Lúc này phải bắt sự kiện filechooser theo nguyên tắc: lắng nghe trước, click sau.
test("Xử lý File Chooser", async ({ page }) => {
// 1. Bắt đầu lắng nghe sự kiện filechooser TRƯỚC
const fileChooserPromise = page.waitForEvent("filechooser");
// 2. Thực hiện click gây ra cửa sổ chọn file
await page.getByRole("button", { name: "Upload File" }).click();
// 3. Chờ promise ở bước 1 hoàn thành
const fileChooser = await fileChooserPromise;
// 4. Gán file cho cửa sổ đó
await fileChooser.setFiles("path/to/my-file.jpg");
});
🔹 3. Download file
Download cũng theo đúng mô hình lắng nghe trước, hành động sau.
test("Xử lý Download File", async ({ page }) => {
// 1. Lắng nghe sự kiện download TRƯỚC
const downloadPromise = page.waitForEvent("download");
// 2. Click gây ra download
await page.getByRole("link", { name: "Tải Báo cáo (PDF)" }).click();
// 3. Chờ download hoàn thành
const download = await downloadPromise;
// 4. Lưu file để kiểm tra
const savePath = "test-results/downloads/" + download.suggestedFilename();
await download.saveAs(savePath);
// 5. Kiểm tra tên file
expect(download.suggestedFilename()).toContain("Bao_Cao");
});
🧩 Phần 2: Shadow DOM (Open và Closed)
Shadow DOM là một "DOM con" được đóng gói bên trong một element host, giúp cô lập CSS và JavaScript. Nó có hai chế độ: open (bên ngoài xem được qua element.shadowRoot) và closed (shadowRoot trả về null).
🔹 1. Shadow DOM Open: tự động xuyên qua
Với shadow DOM mở, Playwright tự động "pierce" (xuyên) vào bên trong, nên bạn chỉ cần viết locator như bình thường.
// Tìm element host
const openHost = page.locator("open-shadow-el#open-shadow-demo");
// Playwright TỰ ĐỘNG xuyên vào #shadow-root để tìm phần tử con
await openHost.locator("#os-input").fill("Hello Shadow");
await openHost.locator("#os-btn").click();
// Kiểm tra kết quả bên trong shadow DOM
await expect(openHost.locator("#os-status")).toHaveText("You typed: Hello Shadow");
🔹 2. Shadow DOM Closed: không thể xuyên qua
Với shadow DOM đóng, bạn giống như đứng trước một "két sắt đã khóa". Chỉ test được chính element host, mọi locator con bên trong đều thất bại.
const closedHost = page.locator("closed-shadow-el#closed-shadow-demo");
// Chỉ có thể kiểm tra chính element host
await expect(closedHost).toBeVisible();
// Locator con sẽ THẤT BẠI vì không xuyên vào được
// await closedHost.locator("#button-ben-trong").click(); // ❌ Lỗi
🖼️ Phần 3: iframe với frameLocator()
Khác với Shadow DOM (cùng một document), thẻ <iframe> nhúng hẳn một trang web khác với document/window riêng. Vì vậy bắt buộc phải dùng frameLocator() để chuyển ngữ cảnh vào trong iframe trước khi tìm phần tử.
// Theo ID
const paymentFrame = page.frameLocator("#payment-iframe");
await paymentFrame.locator("#credit-card-number").fill("1234...");
// Theo title (tốt nhất khi iframe không có ID)
const adFrame = page.frameLocator('[title="Quảng cáo Google"]');
await adFrame.locator(".ad-body").click();
// Theo name
const userFrame = page.frameLocator('[name="user-details-frame"]');
await userFrame.locator("#username").fill("...");
// Theo thứ tự nth() - mong manh nhất, hạn chế dùng
const firstFrame = page.frameLocator("iframe").first();
await firstFrame.locator("body").click();
⚠️ Ghi nhớ: Shadow DOM che giấu HTML trong cùng một document nên Playwright tự xuyên qua (nếu open). iframe nhúng một URL khác nên luôn phải qua
frameLocator().
🔍 Phần 4: Phát hiện ảnh hỏng (Broken Images)
Một thẻ <img> có thể "visible" nhưng nội dung ảnh không tải được (404, 500, CORS). Cách đáng tin cậy nhất để kiểm tra là đọc thuộc tính DOM: ảnh OK khi hội đủ img.complete, naturalWidth > 0 và naturalHeight > 0. Ảnh hỏng thường có kích thước tự nhiên bằng 0.
async function isImageOk(imgLocator) {
await imgLocator.waitFor({ state: "visible" }); // Chờ <img> hiển thị
// Chạy JS trong trình duyệt để kiểm tra 3 thuộc tính
return await imgLocator.evaluate((img) => {
return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;
});
}
// Ảnh hợp lệ -> true
const viteImg = page.locator("xpath=//img[@alt='Vite Logo']");
expect(await isImageOk(viteImg)).toBeTruthy();
// Ảnh lỗi 404 -> false
const broken404 = page.locator("xpath=//img[@alt='Broken 404']");
expect(await isImageOk(broken404)).toBeFalsy();
🔹 Quét toàn bộ ảnh trên trang (Audit)
Khi muốn kiểm tra tất cả ảnh cùng lúc, hãy lặp qua document.images ngay trong trình duyệt và trả về danh sách ảnh hỏng.
const results = await page.evaluate(() => {
// Lấy tất cả thẻ <img> rồi map ra trạng thái ok
return Array.from(document.images).map((img) => ({
src: img.src,
ok: img.complete && img.naturalWidth > 0 && img.naturalHeight > 0,
}));
});
const brokenImages = results.filter((r) => !r.ok);
console.log("Các ảnh bị hỏng:", brokenImages);
expect(brokenImages.length).toBe(0); // Mong đợi không có ảnh nào hỏng
🔹 Ghi nhớ nhanh cho Tester
setInputFiles()là cách chính để upload, chạy được cả khi input bị ẩn.- Có thể upload file "ảo" bằng
Buffermà không cần file thật trên ổ đĩa. - Không có input trong DOM thì bắt sự kiện
filechooser; download thì bắt sự kiệndownload, luôn lắng nghe trước. - Shadow DOM open được Playwright tự xuyên qua; closed thì chỉ test được element host.
- iframe luôn phải vào qua
frameLocator(), ưu tiên theo title hoặc name hơn lànth(). - Phát hiện ảnh hỏng dựa vào
complete,naturalWidth,naturalHeightquaevaluate.
✅ Kết luận: Bài 6 gom những kỹ thuật hay gặp nhưng dễ gây bối rối: file ẩn, iframe, shadow DOM và ảnh hỏng. Khi nắm được bản chất từng loại ranh giới (OS dialog, document con, DOM đóng gói), bạn sẽ chọn đúng công cụ thay vì loay hoay với click() hay waitForTimeout().
