ใช้ Copilot และ Claude สร้าง Playwright Script
บทนี้สอนวิธีใช้ AI assistant เขียน Playwright test ที่อ่านง่ายและเสถียร โดยให้ AI ทำส่วน boilerplate ส่วนคุณดูแลส่วน assertion และ logic หลัก
จบบทนี้แล้วคุณจะ...
- ใช้ AI ช่วยเขียน Playwright test ได้เร็วกว่าเขียนเอง
- รู้วิธี prompt ให้ AI เลือก locator ที่ stable
- จับ pattern script ที่ AI ชอบเขียนผิด เช่น hard-coded wait, weak assert
- ตัดสินใจได้ว่าตรงไหนให้ AI ทำ ตรงไหนต้องเขียนเอง
ส่วนซ้ำ ๆ คือจุดคุ้มที่สุด
AI ทำได้ดีในส่วนเหล่านี้:
- Boilerplate: import, test.describe, fixture setup
- Locator suggestion: แนะนำ selector หลายแบบจาก HTML
- Pattern translation: แปลง manual steps เป็น Playwright API
- Refactor to POM: รวม logic ที่ซ้ำเป็น Page Object
- Add retries / timeouts: เพิ่ม config ที่จำเป็น
ส่วนที่ AI ทำได้ปานกลาง (ต้อง verify หนัก):
- เลือก best locator (อาจเลือก css selector เปราะ)
- เขียน assertion ที่มีความหมาย (มักเขียน
toBeVisibleแทนที่จะ assert ค่าจริง) - handle dynamic content / async race condition
ส่วนที่ AI พังบ่อย:
- เดา selector จาก description ที่กำกวม
- ใช้
waitForTimeout(1000)แทนการ wait แบบ deterministic - mock ข้อมูลที่ไม่ตรงกับ shape จริงของ API
โครงที่ใช้ได้กับ feature ทุกแบบ
You are a senior Playwright test engineer using TypeScript.
Context:
- Project uses Playwright Test, TypeScript, page object pattern
- Base URL: https://app.example.com
- Auth: shared storage state at .auth/user.json
HTML snippet of the page being tested:
[PASTE HTML]
User flow to automate:
1. Navigate to /products
2. Search for "laptop"
3. Click first result
4. Add to cart
5. Verify cart count = 1
Constraints:
- Use getByRole, getByLabel, getByTestId — avoid CSS selectors
- Use Playwright auto-waiting; do NOT use waitForTimeout
- Each test must have at least one meaningful assertion per step
- Keep test under 30 lines
Output: Single test file ready to drop into tests/.
หลัก prompt ที่ทำให้ AI gen script ดี:
- ใส่ HTML snippet จริงให้เห็น (ไม่ใช่ description)
- ระบุ locator strategy ที่ทีมใช้
- ห้าม
waitForTimeoutexplicit ตั้งแต่ prompt - บอก convention POM ของทีม
ดู output ต่างกันชัดเจน
Prompt แย่:
write playwright test for adding product to cart
Output:
test('add to cart', async ({ page }) => {
await page.goto('/');
await page.click('.product');
await page.click('.add-to-cart');
await page.waitForTimeout(2000);
expect(true).toBeTruthy();
});
ปัญหา:
- CSS selector เปราะ
- ใช้
waitForTimeout - assertion =
true.toBeTruthy()ไม่มีความหมาย - ไม่มี navigation flow ครบ
Prompt ดี (ใช้ template ข้างบน):
Output:
import { test, expect } from '@playwright/test';
test.describe('Product cart', () => {
test('user can add a laptop to cart', async ({ page }) => {
await page.goto('/products');
await page.getByRole('searchbox').fill('laptop');
await page.getByRole('button', { name: 'Search' }).click();
const firstResult = page.getByTestId('product-card').first();
await expect(firstResult).toBeVisible();
await firstResult.click();
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
});
ความต่าง:
- locator แบบ role-based, อ่านง่าย
- ไม่มี hard-coded wait
- assertion ตรงกับ business behavior
รู้ pattern แล้วแก้ใน review ได้เร็ว
waitForTimeout(N): AI ชอบใส่เพื่อ "ความปลอดภัย" → เปลี่ยนเป็น expect(locator).toBeVisible() หรือ waitForResponse
Selector แบบ chain ยาว: .container > .row > div:nth-child(2) → เปลี่ยนเป็น getByRole หรือ getByTestId
Assertion อ่อน: expect(page).toHaveURL(/.+/) → เปลี่ยนเป็น URL เฉพาะหรือ assert content
Test ที่ยาวเกิน 50 บรรทัด: AI ชอบเขียนรวมหลาย scenario → split เป็นหลาย test
ไม่จัดการ async: ลืม await หน้า action → ตรวจ await ทุก action
Mock ที่ shape ไม่ตรง: route.fulfill({ json: { foo: 'bar' } }) แต่ frontend คาด schema เต็ม → ดึง shape จาก API จริงก่อน
4 ขั้น ใช้กับทุก feature ใหม่
- Spike แบบ manual: ลองทำ flow เองในเบราว์เซอร์ จด step ที่ทำ + selector ที่เห็น
- Prompt AI ด้วย context: ใส่ HTML, step, convention ทีม → AI gen draft
- Run + review: รัน test จริง ดูว่าผ่านไหม / locator ตรงไหม / assert มีความหมาย
- Refactor: เก็บส่วนที่ซ้ำเป็น Page Object, ลบ wait ที่ไม่จำเป็น
ระยะเวลา: feature เล็ก (1 flow) ~10-15 นาที vs เขียนเอง 30-45 นาที
แบบฝึก: ปรับปรุง script ที่ AI gen มา
ดู Playwright test ด้านล่างที่ AI gen มา ระบุปัญหาและเขียนเวอร์ชันที่ดีขึ้น:
import { test, expect } from '@playwright/test';
test('checkout', async ({ page }) => {
await page.goto('https://shop.example.com');
await page.click('.product:nth-child(1)');
await page.waitForTimeout(3000);
await page.click('button.btn-primary');
await page.waitForTimeout(2000);
await page.fill('#card', '4111111111111111');
await page.click('.checkout-btn');
await page.waitForTimeout(5000);
expect(page.url()).toContain('success');
});
ดูคำตอบแนะนำ
ปัญหา:
- ใช้ CSS selector แทน role-based
- มี
waitForTimeout3 จุด รวม 10 วินาที - assertion ใช้
expect(page.url()).toContainไม่ใช่ Playwright matcher - กรอกบัตรเครดิตจริง = security risk
- ไม่มี
awaitหน้า expect
เวอร์ชันที่ปรับปรุง:
import { test, expect } from '@playwright/test';
test('user completes checkout with test card', async ({ page }) => {
await page.goto('/');
await page.getByTestId('product-card').first().click();
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
await page.getByRole('link', { name: 'Checkout' }).click();
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByRole('button', { name: 'Pay now' }).click();
await expect(page).toHaveURL(/\/order\/success/);
await expect(page.getByText('Order confirmed')).toBeVisible();
});
ความต่าง: locator stable, ไม่มี hard wait, assert มีความหมาย, ใช้ test card stripe ไม่ใช่หมายเลขจริง
สิ่งที่ต้องเอาติดตัวไปจากบทนี้
- AI ช่วยเขียน Playwright boilerplate ได้เร็ว แต่ assertion ต้องคนคุม
- ใส่ HTML จริงและ convention ทีมใน prompt เพื่อลดการเดาของ AI
- Pattern ที่ AI ชอบผิดได้แก่
waitForTimeout, weak assertion, CSS selector เปราะ - รัน test ที่ AI gen จริงทุกครั้ง อย่าเชื่อแค่ที่เห็นในหน้าจอ
- workflow 4 ขั้น (spike → prompt → run → refactor) ใช้ได้กับทุก feature