นี่เป็นหนึ่งในจุดเด่นของ Playwright เลยที่สามารถรันการทดสอบข้าม Browser (Chromium, Firefox, Safari) (จริง ๆ แล้วตัวอื่น ๆ จะเป็นตัวที่เอา 3 ตัวนี้ไปดัดแปลง ฉนั้นเราเทสทั้ง 3 ตัวนี้ ก็ถือว่าเทสตัวอื่น ๆ เกือบหมดแล้ว ถ้ามีแปลก ๆ กว่านี้ ก็มีส่วนแบ่งไม่ถึง 1% คนใช้ต้องรับสภาพเอง) ได้พร้อมกันในการรันครั้งเดียว และรายงานผล (Report) จะถูกรวบรวมมารวมอยู่ในหน้าเดียวกัน โดยแยกให้เห็นชัดเจนว่าผลลัพธ์นั้นมาจาก Browser ตัวไหน
วิธีที่ดีที่สุดคือการตั้งค่าผ่านไฟล์ playwright.config.ts (หรือ .js) ครับ
🛠️ วิธีการตั้งค่าใน playwright.config.ts
ให้คุณไปที่ไฟล์คอนฟิกหลัก แล้วกำหนดค่าในส่วนของ projects โดยแยกตาม Browser ที่ต้องการ ดังนี้ครับ
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
reporter: [
['list'],
['monocart-reporter', {
name: 'ระบบรายงานผลการทดสอบแยกตาม Browser และ Scope',
outputFile: './test-results/report.html',
columns: (defaultColumns) => {
defaultColumns.push({
id: 'testScope',
name: 'Scope',
type: 'string',
width: 150,
style: { fontWeight: 'bold', textAlign: 'center' },
getValue: (row) => {
const filePath = row.location?.file || '';
// 🔥 ปรับ Logic: ถ้ามีชื่อโฟลเดอร์เฉพาะเบราว์เซอร์ให้พ่นตรงตัว ที่เหลือตีเป็น Cross-Browser ทั้งหมดอัตโนมัติ
if (filePath.includes('browser-google-chrome-chromium')) return '🌐 Chrome Only';
if (filePath.includes('browser-mozilla-firefox-gecko')) return '🦊 Firefox Only';
if (filePath.includes('browser-apple-safari-webkit')) return '🍎 Safari Only';
return '🌐 Cross-Browser';
}
});
}
}]
],
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry',
},
projects: [
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'] },
testMatch: '**/*.spec.ts',
// 🔥 สั่งข้ามโฟลเดอร์เฉพาะของ Firefox และ Safari
testIgnore: ['**/browser-mozilla-firefox-gecko/**', '**/browser-apple-safari-webkit*/**'],
},
{
name: 'Mozilla Firefox',
use: { ...devices['Desktop Firefox'] },
testMatch: '**/*.spec.ts',
// 🔥 สั่งข้ามโฟลเดอร์เฉพาะของ Chrome และ Safari
testIgnore: ['**/browser-google-chrome-chromium/**', '**/browser-apple-safari-webkit*/**'],
},
{
name: 'Apple Safari',
use: { ...devices['Desktop Safari'] },
testMatch: '**/*.spec.ts',
// 🔥 สั่งข้ามโฟลเดอร์เฉพาะของ Chrome และ Firefox
testIgnore: ['**/browser-google-chrome-chromium/**', '**/browser-mozilla-firefox-gecko/**'],
}
],
});
เขียนสคริปต์ทดสอบบนทุก Browser
plusmagi-playwright/tests/browser-cross/cross-test.spec.ts
import { test, expect } from '@playwright/test';
import * as path from 'path';
import * as fs from 'fs';
import * as http from 'http'; // 🔥 เพิ่มโมดูล HTTP สำหรับสร้างเซิร์ฟเวอร์จริงในเครื่อง
test.describe('Global Cross-Browser Features', () => {
let server: http.Server;
let localServerUrl: string;
// โหลดเซิร์ฟเวอร์จำลองขึ้นมาก่อนเริ่มรันเทสในไฟล์นี้
test.beforeAll(async () => {
server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<html><body><a href="/download/some-file.txt" download>some-file.txt</a></body></html>');
} else if (req.url === '/download/some-file.txt') {
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename="some-file.txt"'
});
res.end('mock text file content');
} else {
res.writeHead(404);
res.end();
}
});
// ให้ระบบสุ่มพอร์ตว่างที่ปลอดภัยจากระบบปฏิบัติการ (พอร์ต 0 หมายถึงสุ่มพอร์ต)
await new Promise<void>((resolve) => {
server.listen(0, () => {
const address = server.address();
if (address && typeof address !== 'string') {
localServerUrl = `http://localhost:${address.port}`;
}
resolve();
});
});
});
// ปิดเซิร์ฟเวอร์จำลองทันทีเมื่อรันเทสทุกเคสในไฟล์นี้เสร็จสิ้น ป้องกันพอร์ตค้าง
test.afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
// 1. เคสทดสอบการคลิกปุ่ม Custom
test('ควรรับส่ง Click Event บน Custom Component ได้ถูกต้อง', async ({ page }) => {
await page.setContent(`
<div id="custom-btn" role="button" tabindex="0" style="cursor:pointer; padding:10px; background:blue; color:white;">Click Me</div>
<div id="click-status">Waiting</div>
<script>
document.getElementById('custom-btn').addEventListener('click', () => {
document.getElementById('click-status').innerText = 'Clicked';
});
</script>
`);
await page.click('#custom-btn');
await expect(page.locator('#click-status')).toHaveText('Clicked');
});
// 2. เคสทดสอบระบบ Offline
test('แอปพลิเคชันต้องแสดงหน้าต่างเตือนเมื่อระบบอินเทอร์เน็ต Offline', async ({ context, page }) => {
await page.setContent(`
<button id="fetch-data">Load Data</button>
<div id="network-error" style="display:none;">Internet Connection Error</div>
<script>
document.getElementById('fetch-data').addEventListener('click', async () => {
try { await fetch('/api/data'); } catch { document.getElementById('network-error').style.display = 'block'; }
});
</script>
`);
try {
await context.setOffline(true);
await page.click('#fetch-data');
await expect(page.locator('#network-error')).toBeVisible();
} finally {
await context.setOffline(false);
}
});
// 3. เคสทดสอบฟอร์มทั่วไป
test('ระบบฟอร์มต้องกรอกข้อมูลและกด Submit ได้อย่างสมบูรณ์', async ({ page }) => {
await page.setContent(`
<form id="test-form" onsubmit="event.preventDefault(); document.getElementById('output').innerText = 'Done';">
<input type="text" id="username" required />
<button type="submit" id="submit-btn">Submit</button>
</form>
<div id="output"></div>
`);
await page.fill('#username', 'pitt_dev');
await page.click('#submit-btn');
await expect(page.locator('#output')).toHaveText('Done');
});
// 4. เคสทดสอบดาวน์โหลดไฟล์ (เวอร์ชันสมบูรณ์ รองรับการรันแบบ Cross-Browser เคร่งครัด)
test('ทดสอบการดาวน์โหลดไฟล์ และตรวจสอบว่าไฟล์ถูกเขียนลงดิสก์จริง', async ({ page }) => {
// วิ่งไปยังเซิร์ฟเวอร์ภายในเครื่องที่เราจำลองขึ้นมาจริง ๆ
await page.goto(localServerUrl);
const downloadLink = page.getByRole('link', { name: 'some-file.txt', exact: true });
// ดักฟัง Event ดาวน์โหลด (รอบนี้จะทำงานฉลุยทุกเบราว์เซอร์เพราะเป็น Socket เน็ตเวิร์กจริง)
const [download] = await Promise.all([
page.waitForEvent('download'),
downloadLink.click()
]);
expect(download.suggestedFilename()).toBe('some-file.txt');
const savePath = path.join(__dirname, '../downloads', download.suggestedFilename());
await download.saveAs(savePath);
expect(fs.existsSync(savePath)).toBe(true);
try {
fs.unlinkSync(savePath);
} catch (err) {
console.error('ไม่สามารถลบไฟล์ทดสอบได้:', err);
}
});
});
ในอนาคตอาจจะเพิ่มโฟลเดอร์ฟีเจอร์ใหม่อีกหลายโฟลเดอร์ที่ระดับราก เช่น tests/unit-authentication, tests/sales-commission) ตัว Playwright จะดักจับไปรันเป็น Cross-Browser ครบทั้ง 3 เบราว์เซอร์ให้ทันที โดยที่ไม่ต้องกลับมาเปิดไฟล์ Config นี้เพื่อพิมพ์เพิ่มพาทอีกเลยครับตลอดทั้งโปรเจกต์
🚀 วิธีการรันและการดู Report
สั่งรันคำสั่งปกติ
เมื่อคุณรันคำสั่งนี้ Playwright จะวิ่งทดสอบทุกไฟล์กับทุก Browser ที่เราตั้งค่าไว้ใน projects พร้อมกันโดยอัตโนมัติnpx playwright test
รันทุกเบราว์เซอร์เฉพาะโฟลเดอร์นั้น (แนะนำสำหรับการตรวจ Cross-Browser)
คำสั่งนี้จะสั่งให้ Playwright วิ่งเข้าไปรันเฉพาะสคริปต์ที่อยู่ในโฟลเดอร์ unit-authentication แต่จะกระจายรันครบทุกเบราว์เซอร์ (Chrome, Firefox, Safari) ตามที่เราตั้งค่าไว้ใน Config ครับnpx playwright test tests/unit-authentication
เจาะจงรันเฉพาะโฟลเดอร์ + เฉพาะเบราว์เซอร์ที่เลือก (เร็วที่สุด เหมาะกับตอน Dev)
หากคุณกำลังโค้ดฟีเจอร์นี้อยู่ แล้วอยากเทสเร็ว ๆ บน Chrome ตัวเดียว ไม่ต้องรันเบราว์เซอร์อื่นให้เสียเวลา ให้เติมแฟลก --project เข้าไปครับnpx playwright test tests/unit-authentication --project="Google Chrome"
เปิดรันผ่าน UI Mode เจาะจงโฟลเดอร์
ถ้าต้องการเปิดหน้าต่าง UI ของ Playwright ขึ้นมาดูสเต็ปการรัน, ดูหน้าจอสกรีนช็อตแบบ Real-time และเจาะจงดูเฉพาะโฟลเดอร์นี้ ให้ใช้คำสั่งnpx playwright test tests/unit-authentication --ui
ถ้าต้องการรันเฉพาะไฟล์เจาะจง
สามารถพิมพ์ลึกไปถึงชื่อไฟล์ได้เลย เช่นnpx playwright test tests/unit-authentication/login.spec.ts
ผลลัพธ์ใน HTML Report
หลังจากรันเสร็จ หน้าต่าง Report (HTML) จะเปิดขึ้นมา โดยโครงสร้างหน้าตาจะเป็นแบบนี้
- จะมีแถบ Filter (ตัวกรอง) ด้านบนให้คุณเลือกกดดูเฉพาะ
chromium,firefoxหรือwebkitได้ - ในรายชื่อ Test Case แต่ละข้อ จะมีป้ายกำกับ (Tag) เล็กๆ บอกชัดเจน เช่น
example.spec.ts > chromium,example.spec.ts > webkit - หากมี Case ไหนพัง (Failed) คุณสามารถกดเข้าไปดูรายละเอียด Screenshot หรือ Video แยกตาม Browser ตัวที่พังได้เลย
💡 ข้อควรระวังเรื่องทรัพยากรเครื่อง
การรัน 3 Browser พร้อมกันค่อนข้างกิน RAM และ CPU หากเครื่องคอมพิวเตอร์ของคุณเริ่มหน่วงหรือค้าง สามารถจำกัดจำนวนการรันพร้อมกันได้โดยเพิ่ม workers: 1 หรือ workers: 2 ในไฟล์ config เพื่อให้มันทยอยรันทีละตัว แต่สุดท้าย Report ก็ยังจะมารวมอยู่ที่หน้าเดียวกันเหมือนเดิมครับ
อ่านเพิ่มเติม