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: 📤 Làm chủ Upload & Download File

Việc xử lý file (tệp) là một kỹ năng quan trọng. Bài học này sẽ chỉ cho bạn cách upload file hiệu quả, kể cả khi nút upload bị "giả mạo" (ẩn), và cách xử lý download.

Vấn đề lớn nhất: Nút Upload "Giả" 👻

Hầu hết các trang web hiện đại đều "giả mạo" (fake) nút upload. Họ ẩn thẻ <input type="file"> thật đi (vì nó xấu và không tùy chỉnh được) và hiển thị một <button> hoặc <div> đẹp mắt thay thế.

<button id="fancy-button">Chọn Ảnh 🖼️</button>

<input type="file" id="real-input" style="display: none;">

❌ CÁCH LÀM SAI (Anti-Pattern):

await page.locator('#fancy-button').click();

Tại sao sai? Hành động này sẽ mở cửa sổ file của hệ điều hành (Windows Explorer, Mac Finder). Playwright không thể tương tác với cửa sổ này. Test của bạn sẽ bị "treo" (treo) ở đây.

 Cách Upload đúng: setInputFiles (Với Input Hiển thị & Bị ẩn) 🎯

Đây là phương thức được khuyên dùng nhất (chiếm 95% trường hợp) vì nó cực kỳ nhanh và ổn định.

Logic quan trọng: locator.setInputFiles() được thiết kế để bỏ qua (bypass) các kiểm tra "actionability" (như visible hay enabled). Nó có thể gán file cho một <input type="file"> ngay cả khi nó bị ẩn hoàn toàn (display: none hoặc opacity: 0).

Bạn chỉ cần phớt lờ cái nút "giả" và gán file trực tiếp vào cái <input> "thật" bị ẩn.

Kịch bản A: Input Hiển thị (Đơn giản)

HTML
<input type="file" id="file-input">

// Gán 1 file

await page.locator('#file-input').setInputFiles('path/to/my-file.jpg');


Kịch bản B: Input Bị ẩn (Phổ biến nhất)

HTML

<button id="fancy-button">Chọn Ảnh 🖼️</button>

<input type="file" id="real-input" style="display: none;">

// ✅ CÁCH LÀM ĐÚNG:

// Tìm input bị ẩn và gán file trực tiếp

await page.locator('#real-input').setInputFiles('path/to/my-file.jpg');

Playwright sẽ làm việc này ngay lập tức mà không cần click.

 

Upload Nhiều file 🗂️ & Xóa file đã chọn 🗑️

Tất cả đều dùng setInputFiles.

const fileInput = page.locator('#my-multi-file-input');

 A. Upload nhiều file (truyền một mảng)

await fileInput.setInputFiles([

  'path/to/file1.jpg',

  'path/to/file2.png'

]);

 

B. Xóa (hủy) các file đã chọn

// Gán một mảng rỗng []

await fileInput.setInputFiles([]);


Upload file "ảo" (In-memory / Buffer) ☁️

Hữu ích khi bạn muốn upload một file được tạo "động" trong test (ví dụ: file JSON, text, hoặc ảnh chụp màn hình).

Bạn không cần file vật lý, chỉ cần một Buffer.

// 1. Tạo file text "ảo" 🧑‍💻

const myBuffer = Buffer.from('Đây là nội dung file text ảo.');


// 2. Upload buffer đó

await page.locator('#file-input').setInputFiles({

  name: 'ten_file.txt',          // Tên file sẽ hiển thị

  mimeType: 'text/plain',       // Loại file

  buffer: myBuffer              // Nội dung file

});


Ví dụ 2: Chụp ảnh màn hình và upload

const screenshot = await page.screenshot();

await page.locator('#avatar-input').setInputFiles({

  name: 'screenshot.png',

  mimeType: 'image/png',

  buffer: screenshot

});

 

Trường hợp đặc biệt: waitForEvent('filechooser') (Giải pháp 5%) ⏱️

Bạn chỉ dùng cách này khi không có thẻ <input type="file"> trong DOM (rất hiếm).

💡 Quy tắc VÀNG: Lắng nghe TRƯỚC (Listen 👂), Hành động SAU (Act 👆).

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 hành động click (gây ra cửa sổ 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');

});

 

Download file 📥 (Demo thực tế)

Download là hành động ngược lại: bạn click và chờ một sự kiện download.

💡 Quy tắc VÀNG: Lắng nghe TRƯỚC (Listen 👂), Hành động SAU (Act 👆).

test('Xử lý Download File', async ({ page }) => {

  // 1. BẮT ĐẦU lắng nghe sự kiện 'download' TRƯỚC

  const downloadPromise = page.waitForEvent('download');

  // 2. THỰC HIỆN hành động click (gây ra download)

  await page.getByRole('link', { name: 'Tải Báo cáo (PDF)' }).click();


  // 3. CHỜ Promise ở bước 1 hoàn thành

  const download = await downloadPromise;

  // 4. (Quan trọng) Lưu file để kiểm tra 💾

  // (Playwright tự động xóa file tạm, bạn phải 'saveAs' nếu muốn giữ lại)

  const savePath = 'test-results/downloads/' + download.suggestedFilename();

  await download.saveAs(savePath);

  // 5. Kiểm tra (Assert)

  expect(download.suggestedFilename()).toContain('Bao_Cao');

  // (Bạn cũng có thể dùng 'fs' (File System) của Node.js để kiểm tra savePath)

});

 

Phần 2:  "Thế giới ngầm" - Xử lý Shadow DOM và iframes 🕵️‍♂️

Cả Shadow DOM và iframes đều là các kỹ thuật để cô lập (isolate) một phần của trang web, nhưng chúng khác nhau về bản chất và cách chúng ta tương tác.

Shadow DOM (DOM "Bóng") 🗄️

 Nó là gì?

Khái niệm: Là một "DOM con" (a DOM within a DOM) được đóng gói (encapsulated) và ẩn giấu bên trong một element (gọi là "host").

Mục đích: Để tạo ra các Web Components (Thành phần Web) có thể tái sử dụng. Sự đóng gói này đảm bảo:

CSS được cô lập: CSS từ trang chính không "rò rỉ" vào bên trong (ví dụ: button { color: red; } của trang sẽ không ảnh hưởng đến <button> bên trong).

JS được cô lập: JavaScript từ trang chính không thể vô tình truy vấn (document.querySelector) vào bên trong.

Analogy (Phép loại suy):

Hãy coi Shadow DOM như một "Ngăn kéo bí mật". Bạn thấy cái tủ (<my-widget>), nhưng không thấy các linh kiện bên trong (<button>, <div>).

Open vs. Closed:

mode: 'open' (Mở): (Phổ biến nhất) Lập trình viên cho phép JavaScript bên ngoài nhìn vào bên trong (qua thuộc tính element.shadowRoot).

mode: 'closed' (Đóng): (Hiếm gặp) Lập trình viên cố tình "khóa" nó lại. JavaScript bên ngoài không thể truy cập (element.shadowRoot trả về null).

Cách xử lý bằng Playwright

Playwright xử lý 2 chế độ này hoàn toàn khác nhau.

Trường hợp 1: Shadow DOM (OPEN) - Tự động xuyên qua! ✅

Đây là một tin tuyệt vời: Playwright tự động "xuyên" (pierces) qua Shadow DOM mở. Bạn không cần làm gì đặc biệt cả. Cứ viết locator như bình thường.

const panel = page.getByRole('tabpanel', { name: '🧩 Shadow DOM & iFrame' });


// 1. Tìm 'host' element

const openHost = panel.locator('open-shadow-el#open-shadow-demo');


// 2. Playwright TỰ ĐỘNG xuyên vào bên trong #shadow-root

//    để tìm #os-input và #os-btn

await openHost.locator('#os-input').fill('Hello Shadow');

await openHost.locator('#os-btn').click();

// 3. Kiểm tra kết quả bên trong

await expect(openHost.locator('#os-status')).toHaveText('You typed: Hello Shadow');

Kết luận: Với Shadow DOM (Open), chỉ cần nối chuỗi locator bình thường.


Trường hợp 2: Shadow DOM (CLOSED) - Không thể xuyên qua! ❌

Khi mode: 'closed', "cửa" đã bị khóa. Playwright (giống như JavaScript) không thể nhìn vào bên trong.

// 1. Tìm 'host' element (cái "két sắt" đã khóa)

const closedHost = panel.locator('closed-shadow-el#closed-shadow-demo');

// 2. Bạn chỉ có thể test CHÍNH NÓ (cái két sắt)

await expect(closedHost).toBeVisible();


// 3. Bất kỳ locator con nào cũng sẽ THẤT BẠI

//    await closedHost.locator('#button-ben-trong').click(); // ❌ SẼ BÁO LỖI


// 4. (Cách lách luật - Không khuyến nghị)

// Đoạn code này là nỗ lực dùng JavaScript thô (evaluate) để "phá khóa".

// Nó không được khuyến nghị vì vi phạm tính đóng gói và rất mong manh.

// const el = await closedHost.elementHandle();

// const innerText = await page.evaluate(h => h.shadowRoot ? h.shadowRoot.textContent : '(closed)', el);

// expect(innerText).toContain('closed');

Kết luận: Với closed Shadow DOM, bạn chỉ nên test "bề mặt" của nó, không nên cố gắng test những gì bị khóa bên trong.


iframes (Khung nội tuyến) 🖼️

Nó là gì?

Khái niệm: Thẻ <iframe> dùng để nhúng một trang web hoàn toàn khác vào bên trong trang web hiện tại.

Bản chất: <iframe> là một document và window riêng biệt. Nó là một trang web độc lập, có URL, CSS, và JS của riêng nó.

Analogy (Phép loại suy):

Nếu trang của bạn là một căn phòng, <iframe> là một "Cửa sổ nhìn sang phòng khác". Bạn không thể "với tay" sang phòng bên đó (document khác) để lấy đồ (element). Bạn phải "đi qua cửa sổ" trước.

Ứng dụng: Quảng cáo (Ads), Cổng thanh toán (Stripe, PayPal), Video YouTube, Google Maps.

 Cách xử lý bằng Playwright (Bắt buộc dùng frameLocator)

Vì <iframe> là một document khác, page.locator() (chỉ tìm trong document chính) sẽ thất bại.

Bạn bắt buộc phải dùng page.frameLocator() để "chuyển ngữ cảnh" vào bên trong iframe trước.

Trường hợp 1: iFrame có ID (hoặc Selector rõ ràng)

Đây là cách dễ nhất. Bạn dùng CSS hoặc XPath để tìm chính cái thẻ <iframe> đó.

// Giả sử HTML: <iframe id="payment-iframe" src="...">

const paymentFrame = page.frameLocator('#payment-iframe');



// 'paymentFrame' giờ là gốc (root) của document bên trong iframe

await paymentFrame.locator('#credit-card-number').fill('1234...');


Trường hợp 2: iFrame KHÔNG có ID (Cách tìm nâng cao)

Demo 1: Theo title (Cách tốt nhất khi không có ID)

Thuộc tính title rất tốt cho accessibility (hỗ trợ tiếp cận).

<iframe title="Quảng cáo Google" src="..."></iframe>

const adFrame = page.frameLocator('[title="Quảng cáo Google"]');

await adFrame.locator('.ad-body').click();


Demo 2: Theo name

Thường dùng trong các form cũ.

<iframe name="user-details-frame" src="..."></iframe>

const userFrame = page.frameLocator('[name="user-details-frame"]');

await userFrame.locator('#username').fill('...');


Demo 3: Theo thứ tự nth() (Mong manh nhất ⚠️)

Chỉ dùng khi không còn cách nào khác, vì thứ tự iframe có thể thay đổi.

// Lấy iframe ĐẦU TIÊN trên trang

const firstFrame = page.frameLocator('iframe').first();

// hoặc

const secondFrame = page.frameLocator('iframe').nth(1);

await firstFrame.locator('body').click();


Tóm tắt: Shadow DOM vs. iframe

Tính chất

Shadow DOM (Hộp kín 🗄️)

iframe (Cửa sổ 🖼️)

Bản chất

Che giấu HTML (Cùng 1 document)

Nhúng 1 URL khác (Một document khác)

Playwright

Tự động xuyên qua (Viết locator bình thường)

Bắt buộc dùng frameLocator()



Phần 3:🖼️ Phát hiện Ảnh hỏng (Broken Images)

Một trong những vấn đề khó chịu nhất khi kiểm thử UI là ảnh bị hỏng. Một thẻ <img> có thể hiển thị (visible) trên trang, nhưng nội dung ảnh không tải được (ví dụ: do lỗi 404, 500, hoặc lỗi CORS).

Khi đó, await expect(img).toBeVisible() sẽ PASS, nhưng người dùng lại thấy biểu tượng ảnh hỏng 🏞️.

Chúng ta cần các kỹ thuật đặc biệt để phát hiện điều này. Dưới đây là 4 phương pháp, xếp từ đơn giản nhất đến mạnh mẽ nhất.

Dựa vào UI hiển thị trạng thái (Nếu có)

Đây là cách làm đơn giản và được khuyến khích nhất (nếu UI của bạn hỗ trợ).

Nếu lập trình viên đã code để hiển thị một text "ERROR" hoặc "OK" bên cạnh ảnh, hãy test chính cái text đó. Đây là cách "Test what the user sees" (Kiểm tra những gì người dùng thấy) chuẩn nhất.

// Giả sử có <span id="img-status-1">OK</span>

await expect(panel.locator('#img-status-1')).toHaveText('OK');



// Giả sử có <span id="img-status-2">ERROR: 404</span>

await expect(panel.locator('#img-status-2')).toContainText('ERROR');

await expect(panel.locator('#img-status-3')).toContainText('ERROR');

 

Kiểm tra thuộc tính DOM (Cách Kỹ thuật)

Đây là cách làm phổ biến nhất khi UI không có text báo lỗi. Chúng ta sẽ dùng page.evaluate() để kiểm tra các thuộc tính "ẩn" của thẻ <img> mà chỉ trình duyệt mới biết.

"Công thức" kiểm tra ảnh OK:

Một ảnh được coi là tải thành công khi có cả 3 điều kiện:

img.complete: Trình duyệt đã thử tải xong (dù thành công hay thất bại).

img.naturalWidth > 0: Kích thước "tự nhiên" (chiều rộng gốc) của ảnh phải lớn hơn 0.

img.naturalHeight > 0: Kích thước "tự nhiên" (chiều cao gốc) của ảnh phải lớn hơn 0.

Nếu ảnh hỏng (lỗi 404), naturalWidth và naturalHeight của nó sẽ là 0.

async function isImageOk(imgLocator) {

  // Chờ cho thẻ <img> được visible

  await imgLocator.waitFor({ state: 'visible' });

  // Chạy code JS trong trình duyệt để check 3 thuộc tính

  return await imgLocator.evaluate((img) => {

    return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;

  });

}

// === Áp dụng ===

// 1. Ảnh hợp lệ

const viteImg = panel.locator("xpath=//img[@alt='Vite Logo']");

await expect(await isImageOk(viteImg)).toBeTruthy(); // Mong đợi là TRUE


// 2. Ảnh hỏng (404)

const broken404 = panel.locator("xpath=//img[@alt='Broken 404']");

await expect(await isImageOk(broken404)).toBeFalsy(); // Mong đợi là FALSE


// 3. Ảnh hỏng (CORS)

const brokenCors = panel.locator("xpath=//img[@alt='Broken CORS']");

await expect(await isImageOk(brokenCors)).toBeFalsy(); // Mong đợi là FALSE

<hr size=2 width="100%" noshade style='color:gray' align=center>

 

Chờ Network Response (Cách Nâng cao)

Cách này rất mạnh mẽ vì nó không quan tâm đến DOM, mà "lắng nghe" trực tiếp các yêu cầu mạng (network requests) của trang.

Bạn có thể ra lệnh cho Playwright chờ một yêu cầu mạng (response) nào đó và kiểm tra xem nó có ok() (status 200) hay không.

// 1. Bắt đầu chờ

const responsePromise = page.waitForResponse(res =>

  res.url().includes('not-found.png') && !res.ok()

);


// 2. (Click hoặc tải trang - một hành động nào đó gây ra việc tải ảnh)

// ...


// 3. Chờ và xác nhận

const response = await responsePromise;

expect(response.status()).toBe(404); // Xác nhận nó là lỗi 404

Bạn cũng có thể dùng page.on('response', ...) để kiểm tra tất cả các response và thu thập mọi ảnh bị lỗi 404/500.

Quét toàn bộ ảnh trên trang (Dùng cho Audit)

Cách này hữu ích khi bạn muốn làm một bài test "audit" (kiểm tra tổng thể) để đảm bảo không có bất kỳ ảnh nào trên trang bị hỏng.

Nó dùng page.evaluate để lặp qua document.images (danh sách tất cả ảnh của trình duyệt) và áp dụng "công thức" ở Cách 2 cho tất cả.

 

const results = await panel.evaluate(() => {

  // Array.from(document.images) -> Lấy tất cả thẻ <img>

  return Array.from(document.images).map((img) => ({

    src: img.src,

    ok: img.complete && img.naturalWidth > 0 && img.naturalHeight > 0,

  }));

});


// In ra danh sách ảnh hỏng

const brokenImages = results.filter(r => !r.ok);

console.log('Các ảnh bị hỏng:', brokenImages);


// Kiểm tra (Assert)

expect(brokenImages.length).toBe(0); // Mong đợi không có ảnh nào hỏng

Teacher

Teacher

Nguyên Hoàng

Automation Engineer

With 7+ years of hands-on experience across multiple languages and frameworks. I'm here to share knowledge, helping you turn complex processes into simple and effective solutions.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học