Playwright Cheat Sheet: ฟังก์ชันที่ใช้บ่อย วิธีใช้ และตัวอย่างผลลัพธ์
page = หน้าเว็บ, locator = ตัวชี้ element, ผลลัพธ์ที่แสดงเป็นตัวอย่างจากการรันจริงบน saucedemo1. Locator: การหา element (ใช้บ่อยที่สุด)
Locator เป็น lazy และ auto-wait: ยังไม่ค้นหา element จนกว่าจะสั่ง action หรือ assert
1.1 page.getByRole(role, options) (แนะนำอันดับ 1)
หาจาก role ตาม accessibility เช่น button, link, heading, textbox, checkbox
await page.getByRole('button', { name: 'Login' }).click();
await page.getByRole('heading', { name: 'Products' });
await page.getByRole('link', { name: /twitter/i }); // regex ได้
await page.getByRole('button', { name: 'Add to cart', exact: true });ผลลัพธ์: คืนค่าเป็น Locator (ไม่ error ถ้ายังไม่เจอ จนกว่าจะใช้งานจริง)
1.2 page.getByText(text, options)
await page.getByText('Sauce Labs Backpack').click();
await page.getByText('Swag Labs', { exact: true });
await expect(page.getByText(/thank you/i)).toBeVisible();1.3 page.getByLabel(text)
หา input จาก label ที่ผูกไว้ (เหมาะกับฟอร์ม)
await page.getByLabel('Email').fill('a@b.com');1.4 page.getByPlaceholder(text)
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');1.5 page.getByTestId(id)
ค่าเริ่มต้นอ่านจาก data-testid ถ้าเว็บใช้ data-test (เช่น saucedemo) ให้ตั้งใน config
// playwright.config.ts
use: { testIdAttribute: 'data-test' }
// ในเทสต์
await page.getByTestId('login-button').click();1.6 page.locator(selector)
ใช้ CSS / XPath / text selector ตรงๆ เมื่อวิธีข้างบนไม่พอ
page.locator('[data-test="username"]');
page.locator('.inventory_item');
page.locator('xpath=//button[text()="Login"]');
page.locator('div.item >> text=Backpack');1.7 getByAltText / getByTitle
await page.getByAltText('Sauce Labs Backpack').click(); // img alt
await page.getByTitle('Close').click(); // title attr1.8 การเลือกตำแหน่ง: first(), last(), nth()
const items = page.locator('.inventory_item');
await items.first().click();
await items.last().click();
await items.nth(2).click(); // index เริ่มที่ 01.9 locator.filter({ hasText, has, hasNot, hasNotText })
กรอง element จากกลุ่มที่ locator เจอ
const backpack = page.locator('.inventory_item').filter({ hasText: 'Backpack' });
await backpack.getByRole('button', { name: 'Add to cart' }).click();
const withButton = page.locator('.inventory_item').filter({
has: page.getByRole('button', { name: 'Remove' }),
});1.10 Chaining, and(), or()
// ค้นหาต่อเนื่องภายใน element แม่
await page.locator('.cart_item').getByText('Backpack');
// ต้องตรงทั้งสองเงื่อนไข
page.getByRole('button').and(page.getByTitle('Subscribe'));
// อย่างใดอย่างหนึ่ง (เช่น popup ที่อาจขึ้นหรือไม่ขึ้น)
const dialog = page.getByText('Accept cookies').or(page.getByText('ยอมรับคุกกี้'));
await dialog.click();1.11 locator.count() และ locator.all()
const n = await page.locator('.inventory_item').count();
console.log(n);
for (const item of await page.locator('.inventory_item').all()) {
console.log(await item.innerText());
}ผลลัพธ์:
6
Sauce Labs Backpack
$29.99
ADD TO CART
...2. Navigation: การเปิดหน้าและเปลี่ยนหน้า
2.1 page.goto(url, options)
await page.goto('https://www.saucedemo.com');
await page.goto('/inventory.html'); // ใช้ baseURL จาก config
await page.goto(url, { waitUntil: 'domcontentloaded' });ตัวเลือก waitUntil: load (ค่าเริ่มต้น), domcontentloaded, networkidle, commit
2.2 page.reload(), page.goBack(), page.goForward()
await page.reload();
await page.goBack();
await page.goForward();2.3 page.url() และ page.title()
console.log(page.url()); // sync ไม่ต้อง await
console.log(await page.title());ผลลัพธ์:
https://www.saucedemo.com/inventory.html
Swag Labs2.4 page.waitForURL(url)
รอให้ URL เปลี่ยนตามที่ต้องการ (เช่น หลัง redirect)
await page.getByTestId('login-button').click();
await page.waitForURL('**/inventory.html');
await page.waitForURL(/checkout-complete/);3. Actions: การกระทำกับ element
ทุก action จะ auto-wait จน element visible, stable, enabled และรับ event ได้ ก่อนลงมือทำ
3.1 locator.click(options)
await page.getByTestId('login-button').click();
await locator.click({ button: 'right' }); // คลิกขวา
await locator.click({ modifiers: ['Shift'] }); // กด Shift ค้าง
await locator.click({ position: { x: 10, y: 10 } });
await locator.click({ force: true }); // ข้ามการตรวจ actionability
await locator.click({ clickCount: 2 });
await locator.click({ timeout: 5000 });3.2 locator.fill(value)
ล้างค่าเดิมแล้วใส่ค่าใหม่ทันที (ใช้กับ input, textarea)
await page.getByTestId('username').fill('standard_user');
await page.getByTestId('password').fill('secret_sauce');3.3 locator.clear()
await page.getByTestId('username').clear();3.4 locator.pressSequentially(text, { delay })
พิมพ์ทีละตัวอักษรเหมือนผู้ใช้จริง (แทน type() ที่เลิกแนะนำแล้ว) ใช้เมื่อเว็บมี autocomplete หรือฟัง keydown
await page.getByPlaceholder('Search').pressSequentially('shoes', { delay: 100 });3.5 locator.press(key)
await page.getByTestId('password').press('Enter');
await locator.press('Control+A');
await locator.press('ArrowDown');
await locator.press('Tab');3.6 locator.check(), uncheck(), setChecked()
await page.getByLabel('Remember me').check();
await page.getByLabel('Remember me').uncheck();
await page.getByLabel('Subscribe').setChecked(true);3.7 locator.selectOption(value)
เลือกค่าใน dropdown (<select>) ได้จาก value, label หรือ index
await page.locator('[data-test="product-sort-container"]').selectOption('hilo');
await select.selectOption({ label: 'Price (high to low)' });
await select.selectOption({ index: 2 });
await select.selectOption(['red', 'blue']); // multi-selectผลลัพธ์: คืน array ของ value ที่ถูกเลือก เช่น ['hilo']
3.8 locator.hover(), focus(), blur()
await page.getByText('Menu').hover();
await page.getByLabel('Email').focus();3.9 locator.dblclick(), tap()
await locator.dblclick();
await locator.tap(); // สำหรับ touch device / mobile emulation3.10 locator.setInputFiles(files)
อัปโหลดไฟล์ผ่าน <input type=file>
await page.locator('input[type=file]').setInputFiles('tests/data/photo.png');
await page.locator('input[type=file]').setInputFiles(['a.png', 'b.png']);
await page.locator('input[type=file]').setInputFiles([]); // ล้างไฟล์3.11 locator.dragTo(target)
await page.locator('#item').dragTo(page.locator('#dropzone'));3.12 locator.scrollIntoViewIfNeeded()
await page.locator('.footer').scrollIntoViewIfNeeded();4. Assertions: การตรวจสอบด้วย expect (ใช้บ่อยมาก)
expect(locator) แบบ web-first จะ retry อัตโนมัติ จนผ่านหรือหมดเวลา (ค่าเริ่มต้น 5 วินาที) ต่างจาก expect(value) ธรรมดาที่ตรวจครั้งเดียว
4.1 สถานะ element
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toBeEditable();
await expect(locator).toBeFocused();
await expect(locator).toBeEmpty();
await expect(locator).toBeAttached();4.2 ข้อความ
await expect(locator).toHaveText('Thank you for your order!'); // ตรงทั้งหมด
await expect(locator).toContainText('Thank you'); // มีบางส่วน
await expect(locator).toHaveText(/thank you/i); // regex
await expect(items).toHaveText(['Backpack', 'Bike Light']); // หลาย element ตามลำดับ4.3 ค่าและ attribute
await expect(input).toHaveValue('standard_user');
await expect(link).toHaveAttribute('href', '/inventory.html');
await expect(el).toHaveClass(/active/);
await expect(el).toHaveCSS('color', 'rgb(255, 0, 0)');
await expect(el).toHaveId('main');4.4 จำนวน
await expect(page.locator('.inventory_item')).toHaveCount(6);
await expect(page.locator('.error')).toHaveCount(0);4.5 หน้าเว็บ
await expect(page).toHaveURL('https://www.saucedemo.com/checkout-complete.html');
await expect(page).toHaveURL(/inventory/);
await expect(page).toHaveTitle('Swag Labs');4.6 Assert กับค่าทั่วไป (ไม่ retry)
expect(29.99).toBeGreaterThan(9.99);
expect(prices[0]).toBeGreaterThanOrEqual(prices[1]);
expect(total).toBeCloseTo(32.39, 2); // ทศนิยม
expect(names).toContain('Sauce Labs Backpack');
expect(names).toEqual(['A', 'B']);
expect(obj).toMatchObject({ id: 1 });
expect(text).toMatch(/[$][0-9]+/);
expect(list).toHaveLength(2);
expect(value).toBeTruthy();4.7 ตัวปรับแต่ง: not, soft, poll, message, timeout
await expect(locator).not.toBeVisible(); // กลับเงื่อนไข
await expect.soft(locator).toHaveText('A'); // fail แล้วเทสต์ยังรันต่อ
await expect.soft(locator2).toHaveText('B');
await expect.poll(async () => { // poll ค่า async ซ้ำจนผ่าน
const res = await request.get('/api/status');
return (await res.json()).state;
}, { timeout: 10000 }).toBe('done');
await expect(locator, 'ปุ่ม Login ต้องเห็น').toBeVisible({ timeout: 10000 });ตัวอย่างผลลัพธ์เมื่อ assert ล้มเหลว:
Error: expect(locator).toHaveText(expected)
Locator: locator('[data-test="complete-header"]')
Expected string: "Thank you for your order!"
Received string: ""
Timeout: 5000ms4.8 Visual / Snapshot
await expect(page).toHaveScreenshot('home.png');
await expect(locator).toHaveScreenshot();
await expect(page).toHaveScreenshot({ maxDiffPixels: 100, fullPage: true });
expect(await page.content()).toMatchSnapshot('page.html');
await expect(locator).toMatchAriaSnapshot();รันครั้งแรกจะสร้างภาพอ้างอิง ใช้ npx playwright test --update-snapshots เพื่ออัปเดต
5. การดึงข้อมูลจากหน้าเว็บ (Dynamic Inspection)
5.1 locator.textContent() และ innerText()
const name = await page.locator('.inventory_item_name').first().textContent();
const price = await page.locator('.inventory_item_price').first().innerText();
console.log(name, price);ผลลัพธ์:
Sauce Labs Backpack $29.99ความต่าง: textContent รวมข้อความที่ซ่อนอยู่และอาจมี whitespace, innerText ได้เฉพาะข้อความที่มองเห็นและจัดรูปแล้ว
5.2 allTextContents() และ allInnerTexts()
ดึงจากหลาย element เป็น array
const prices = await page.locator('.inventory_item_price').allTextContents();
console.log(prices);ผลลัพธ์:
[ '$29.99', '$9.99', '$15.99', '$49.99', '$7.99', '$15.99' ]5.3 inputValue()
const v = await page.getByTestId('username').inputValue();
console.log(v); // standard_user5.4 getAttribute(name)
const href = await page.locator('.shopping_cart_link').getAttribute('href');
console.log(href); // /cart.html5.5 isVisible(), isEnabled(), isChecked()
คืน boolean ทันที ไม่ retry (ต่างจาก expect) ใช้ใน if เท่านั้น
if (await page.getByText('Accept').isVisible()) {
await page.getByText('Accept').click();
}
console.log(await checkbox.isChecked()); // false5.6 locator.evaluate() และ evaluateAll()
รัน JavaScript ในหน้าเว็บ
const color = await locator.evaluate(el => getComputedStyle(el).color);
const names = await page.locator('.inventory_item_name')
.evaluateAll(els => els.map(e => e.textContent));
const height = await page.evaluate(() => document.body.scrollHeight);5.7 locator.boundingBox()
console.log(await locator.boundingBox());
// { x: 20, y: 140, width: 240, height: 360 }5.8 page.content()
const html = await page.content(); // HTML ทั้งหน้าเป็น string5.9 แปลงราคาเป็นตัวเลข (pattern ที่ใช้บ่อย)
const toNumber = (t: string) => parseFloat(t.replace(/[^0-9.]/g, ''));
const prices = (await page.locator('.inventory_item_price').allTextContents()).map(toNumber);
console.log(Math.max(...prices), Math.min(...prices)); // 49.99 7.996. Waiting: การรอ
ส่วนใหญ่ไม่ต้องรองเอง เพราะ auto-wait และ expect จัดการให้แล้ว ใช้เมื่อจำเป็นเท่านั้น
6.1 locator.waitFor({ state })
await page.locator('.spinner').waitFor({ state: 'hidden' });
await page.locator('.result').waitFor({ state: 'visible', timeout: 10000 });state: attached, detached, visible, hidden
6.2 page.waitForLoadState(state)
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');6.3 page.waitForResponse() / waitForRequest()
รอ API call หลังจากกดปุ่ม ให้ครอบด้วย Promise.all เพื่อกัน race condition
const [response] = await Promise.all([
page.waitForResponse(r => r.url().includes('/api/orders') && r.status() === 200),
page.getByText('Submit').click(),
]);
console.log(response.status(), await response.json());ผลลัพธ์:
200 { id: 101, status: 'created' }6.4 page.waitForFunction(fn)
await page.waitForFunction(() => document.querySelectorAll('.item').length > 5);6.5 page.waitForTimeout(ms)
หน่วงเวลาตายตัว ใช้เฉพาะตอน debug เพราะทำให้เทสต์ช้าและไม่เสถียร
await page.waitForTimeout(2000);6.6 page.waitForEvent(event)
const popup = await Promise.all([
page.waitForEvent('popup'),
page.getByText('Open').click(),
]).then(([p]) => p);7. โครงสร้างเทสต์ (Test Runner)
7.1 test() และ test.describe()
import { test, expect } from '@playwright/test';
test.describe('Checkout flow', () => {
test('login สำเร็จ', async ({ page }) => {
await page.goto('https://www.saucedemo.com');
// ...
});
test('login ผิดพลาด', async ({ page }) => {
// ...
});
});ฟิกซ์เจอร์ที่ใช้บ่อย: page, context, browser, request, browserName
7.2 Hooks
test.beforeAll(async () => { /* รันครั้งเดียวก่อนทุกเทสต์ */ });
test.beforeEach(async ({ page }) => {
await page.goto('https://www.saucedemo.com');
});
test.afterEach(async ({ page }) => { /* เคลียร์ข้อมูล */ });
test.afterAll(async () => { /* ปิดการเชื่อมต่อ */ });7.3 test.step() จัดกลุ่มขั้นตอนในรายงาน
await test.step('Login', async () => {
await page.getByTestId('username').fill('standard_user');
await page.getByTestId('login-button').click();
});
await test.step('Sort by price', async () => {
await page.getByTestId('product-sort-container').selectOption('hilo');
});ใน HTML report จะแสดงเป็นขั้นตอนที่พับเก็บได้ พร้อมเวลาของแต่ละ step
7.4 Skip / Only / Fixme / Slow / Tag
test.skip('ยังไม่พร้อม', async () => {});
test.only('รันเฉพาะอันนี้', async () => {});
test.fixme('บั๊กที่รู้อยู่แล้ว', async () => {});
test.slow(); // เพิ่ม timeout 3 เท่า
test.skip(browserName === 'webkit', 'ไม่รองรับ');
test('smoke', { tag: '@smoke' }, async ({ page }) => {});รันเฉพาะแท็ก: npx playwright test --grep @smoke
7.5 Data-driven (วนรันหลายชุดข้อมูล)
const users = ['standard_user', 'problem_user', 'performance_glitch_user'];
for (const u of users) {
test(`login ด้วย ${u}`, async ({ page }) => {
await page.goto('https://www.saucedemo.com');
await page.getByTestId('username').fill(u);
await page.getByTestId('password').fill('secret_sauce');
await page.getByTestId('login-button').click();
await expect(page).toHaveURL(/inventory/);
});
}7.6 Custom fixture
import { test as base } from '@playwright/test';
const test = base.extend<{ loggedInPage: Page }>({
loggedInPage: async ({ page }, use) => {
await page.goto('https://www.saucedemo.com');
await page.getByTestId('username').fill('standard_user');
await page.getByTestId('password').fill('secret_sauce');
await page.getByTestId('login-button').click();
await use(page);
},
});
test('ใช้ที่ login แล้ว', async ({ loggedInPage }) => {
await expect(loggedInPage).toHaveURL(/inventory/);
});7.7 ผลลัพธ์การรันที่พบบ่อย
Running 3 tests using 3 workers
✓ 1 [chromium] › login.spec.ts:5:1 › login สำเร็จ (1.8s)
✓ 2 [chromium] › login.spec.ts:12:1 › login ผิดพลาด (1.2s)
✘ 3 [chromium] › cart.spec.ts:8:1 › checkout (6.1s)
1 failed
2 passed (9.4s)8. Browser, Context, Page
เมื่อใช้ @playwright/test ระบบสร้างให้อัตโนมัติ ส่วนนี้ใช้เมื่อเขียนสคริปต์เองหรือต้องการควบคุมละเอียด
8.1 เปิด browser เอง
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: false, slowMo: 300 });
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
const page = await context.newPage();
await page.goto('https://www.saucedemo.com');
await browser.close();browser ที่ใช้ได้: chromium, firefox, webkit
8.2 ตัวเลือกของ context
await browser.newContext({
viewport: { width: 390, height: 844 },
locale: 'th-TH',
timezoneId: 'Asia/Bangkok',
geolocation: { latitude: 13.75, longitude: 100.5 },
permissions: ['geolocation'],
userAgent: 'MyAgent/1.0',
colorScheme: 'dark',
httpCredentials: { username: 'u', password: 'p' },
});จำลองมือถือ: const { devices } = require('playwright'); browser.newContext({ ...devices['iPhone 13'] })
8.3 บันทึกและใช้ session ซ้ำ (storageState)
login ครั้งเดียวแล้วนำไปใช้กับทุกเทสต์ ประหยัดเวลามาก
// บันทึกหลัง login
await context.storageState({ path: 'auth.json' });
// ใช้ใน config
use: { storageState: 'auth.json' }
// หรือใช้ในไฟล์เทสต์เดียว
test.use({ storageState: 'auth.json' });8.4 หลายแท็บและ popup
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.getByText('Open in new tab').click(),
]);
await newPage.waitForLoadState();
console.log(await newPage.title());
console.log(context.pages().length); // 2
await newPage.close();8.5 Cookies และ Storage
await context.addCookies([{ name: 'token', value: 'abc', url: 'https://www.saucedemo.com' }]);
console.log(await context.cookies());
await context.clearCookies();
await page.evaluate(() => localStorage.setItem('k', 'v'));
const v = await page.evaluate(() => localStorage.getItem('k'));9. Frames, Dialogs, Downloads
9.1 iframe
const frame = page.frameLocator('#payment-iframe');
await frame.getByLabel('Card number').fill('4242424242424242');
await frame.getByRole('button', { name: 'Pay' }).click();9.2 Dialog (alert, confirm, prompt)
ต้องผูก handler ก่อนที่ dialog จะเด้ง
page.on('dialog', async dialog => {
console.log(dialog.type(), dialog.message());
await dialog.accept(); // หรือ dialog.dismiss() / accept('ข้อความ')
});
await page.getByText('Delete').click();ผลลัพธ์:
confirm Are you sure you want to delete?9.3 ดาวน์โหลดไฟล์
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByText('Export CSV').click(),
]);
console.log(download.suggestedFilename()); // report.csv
await download.saveAs('downloads/' + download.suggestedFilename());9.4 Shadow DOM, Keyboard, Mouse
// Locator ทะลุ shadow DOM ให้อัตโนมัติ
await page.locator('my-component').getByText('OK').click();
await page.keyboard.type('Hello');
await page.keyboard.press('Control+A');
await page.keyboard.down('Shift');
await page.mouse.move(100, 200);
await page.mouse.click(100, 200);
await page.mouse.wheel(0, 500); // scroll ลง10. Network: ดักและจำลอง request
10.1 page.route() mock response
await page.route('**/api/products', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Mock item', price: 9.99 }]),
})
);10.2 route.abort() และ route.continue()
// บล็อกรูปภาพเพื่อให้โหลดเร็วขึ้น
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
// แก้ header แล้วส่งต่อ
await page.route('**/api/**', route =>
route.continue({ headers: { ...route.request().headers(), 'x-test': '1' } })
);
// จำลอง server error
await page.route('**/api/orders', route => route.fulfill({ status: 500 }));10.3 ฟัง event request / response
page.on('request', req => console.log('>>', req.method(), req.url()));
page.on('response', res => console.log('<<', res.status(), res.url()));
page.on('console', msg => console.log('console:', msg.text()));
page.on('pageerror', err => console.log('error:', err.message));ผลลัพธ์:
>> GET https://www.saucedemo.com/
<< 200 https://www.saucedemo.com/
console: Failed to load resource: 40410.4 request fixture (ทดสอบ API ตรง ๆ)
test('API test', async ({ request }) => {
const res = await request.post('https://api.example.com/users', {
data: { name: 'Somchai' },
headers: { Authorization: 'Bearer xxx' },
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(201);
const body = await res.json();
expect(body.name).toBe('Somchai');
});เมธอดอื่น: request.get(), put(), patch(), delete(), head(), fetch()
11. Screenshot, Video, Trace
11.1 Screenshot
await page.screenshot({ path: 'home.png' });
await page.screenshot({ path: 'full.png', fullPage: true });
await locator.screenshot({ path: 'item.png' });
const buffer = await page.screenshot(); // ได้ Buffer11.2 PDF (เฉพาะ Chromium headless)
await page.pdf({ path: 'page.pdf', format: 'A4' });11.3 Video และ Trace
// playwright.config.ts
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry', // หรือ 'on', 'retain-on-failure'
}เปิดดู trace ย้อนหลังทีละ step:
npx playwright show-trace test-results/xxx/trace.zip12. Debug
await page.pause(); // เปิด Inspector หยุดรอให้ debug ด้วยมือnpx playwright test --debug # เปิด Inspector
npx playwright test --ui # UI mode (แนะนำ)
PWDEBUG=1 npx playwright test # Inspector ผ่าน env
DEBUG=pw:api npx playwright test # log ทุก API call13. ไฟล์ตั้งค่า playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000, // timeout ต่อเทสต์
expect: { timeout: 5_000 }, // timeout ของ expect
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: 'https://www.saucedemo.com',
testIdAttribute: 'data-test',
headless: true,
viewport: { width: 1280, height: 720 },
actionTimeout: 10_000,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
});14. คำสั่ง CLI ที่ใช้บ่อย
npm init playwright@latest # สร้างโปรเจกต์
npx playwright install # ติดตั้ง browser
npx playwright install --with-deps # รวม dependency ของ OS
npx playwright test # รันทั้งหมด
npx playwright test login.spec.ts # รันไฟล์เดียว
npx playwright test -g 'checkout' # กรองตามชื่อเทสต์
npx playwright test --project=chromium # เลือก browser
npx playwright test --headed # เห็น browser
npx playwright test --workers=1 # รันทีละเทสต์
npx playwright test --retries=2
npx playwright test --repeat-each=5 # รันซ้ำ ใช้หาเทสต์ flaky
npx playwright test --last-failed # รันเฉพาะที่พังรอบก่อน
npx playwright test --update-snapshots
npx playwright test --reporter=line
npx playwright show-report # เปิด HTML report
npx playwright show-trace trace.zip # เปิด trace
npx playwright codegen https://www.saucedemo.com
npx playwright codegen --device='iPhone 13' --save-storage=auth.json URL15. Best Practices แบบสรุป
ให้ใช้ getByRole หรือ getByTestId ก่อน locator แบบ CSS ที่ผูกกับโครงสร้างหน้า และใช้ expect(locator) แทนการอ่านค่าแล้วเทียบเอง เพราะมี auto-retry หลีกเลี่ยง waitForTimeout ให้ใช้ waitFor, waitForResponse หรือ expect แทน ทำให้แต่ละเทสต์เป็นอิสระต่อกัน (login ด้วย storageState แทนการพึ่งเทสต์ก่อนหน้า) ใช้ toBeCloseTo กับตัวเลขทศนิยม เช่น ราคาและภาษี และเก็บ trace กับ screenshot ไว้เฉพาะตอนเทสต์พังเพื่อประหยัดพื้นที่
เอกสารทางการ: https://playwright.dev/docs/api/class-playwright
16. อันดับฟังก์ชันที่ต้องรู้ก่อน (เรียงจากใช้บ่อยสุด)
สัญลักษณ์: ⭐⭐⭐ = ใช้ทุกเทสต์ / ⭐⭐ = ใช้บ่อย / ⭐ = ใช้เป็นครั้งคราว
16.1 Playwright Top 25
| อันดับ | ฟังก์ชัน | ความถี่ | ใช้ทำอะไร |
|---|---|---|---|
| 1 | page.goto(url) | ⭐⭐⭐ | เปิดหน้าเว็บ |
| 2 | locator.click() | ⭐⭐⭐ | คลิก |
| 3 | locator.fill(text) | ⭐⭐⭐ | กรอกช่องข้อความ |
| 4 | expect(locator).toBeVisible() | ⭐⭐⭐ | ตรวจว่า element แสดงอยู่ |
| 5 | page.getByRole() | ⭐⭐⭐ | หา element ตาม role + ชื่อ |
| 6 | expect(locator).toHaveText() | ⭐⭐⭐ | ตรวจข้อความ |
| 7 | expect(page).toHaveURL() | ⭐⭐⭐ | ตรวจ URL |
| 8 | page.locator(css) | ⭐⭐⭐ | หาด้วย CSS / XPath |
| 9 | page.getByTestId() | ⭐⭐⭐ | หาด้วย data-testid |
| 10 | page.getByText() | ⭐⭐ | หาด้วยข้อความ |
| 11 | locator.selectOption() | ⭐⭐ | เลือก dropdown |
| 12 | locator.allTextContents() | ⭐⭐ | ดึงข้อความหลาย element เป็น array |
| 13 | locator.count() | ⭐⭐ | นับจำนวน element |
| 14 | expect(locator).toHaveCount() | ⭐⭐ | ตรวจจำนวน element |
| 15 | locator.textContent() / innerText() | ⭐⭐ | ดึงข้อความ element เดียว |
| 16 | test.beforeEach() | ⭐⭐ | เตรียมก่อนทุกเทสต์ |
| 17 | test.step() | ⭐⭐ | จัดกลุ่มขั้นตอนในรายงาน |
| 18 | locator.filter() | ⭐⭐ | กรอง element ที่เจอ |
| 19 | locator.check() / uncheck() | ⭐⭐ | ติ๊ก checkbox / radio |
| 20 | page.waitForResponse() | ⭐⭐ | รอ API response |
| 21 | locator.waitFor() | ⭐⭐ | รอ element เปลี่ยนสถานะ |
| 22 | page.screenshot() | ⭐ | ถ่ายภาพหน้าจอ |
| 23 | page.route() | ⭐ | mock / ดัก request |
| 24 | locator.press() | ⭐ | กดแป้นพิมพ์ |
| 25 | request.get/post() | ⭐ | ทดสอบ API |
16.2 TypeScript Top 25 (ฟังก์ชันกับค่าตัวแปร)
| อันดับ | ฟังก์ชัน | ชนิด | ใช้ทำอะไร |
|---|---|---|---|
| 1 | array.map() | Array | แปลงทุกสมาชิก |
| 2 | array.filter() | Array | คัดสมาชิกตามเงื่อนไข |
| 3 | array.forEach() / for...of | Array | วนลูป |
| 4 | array.length | Array/String | จำนวนสมาชิก / ความยาว |
| 5 | array.includes() / string.includes() | Array/String | มีค่านี้หรือไม่ |
| 6 | array.find() | Array | หาตัวแรกที่ตรงเงื่อนไข |
| 7 | array.reduce() | Array | รวมค่าเป็นค่าเดียว (เช่น ผลรวม) |
| 8 | array.sort() | Array | เรียงลำดับ |
| 9 | string.replace() | String | แทนที่ข้อความ |
| 10 | string.trim() | String | ตัดช่องว่างหัวท้าย |
| 11 | string.split() | String | ตัดสตริงเป็น array |
| 12 | array.join() | Array | รวม array เป็นสตริง |
| 13 | parseFloat() / Number() | Number | แปลงสตริงเป็นเลข |
| 14 | number.toFixed() | Number | ปัดทศนิยม (ได้สตริง) |
| 15 | Math.max() / Math.min() | Math | ค่าสูงสุด / ต่ำสุด |
| 16 | Math.round() / floor() / ceil() | Math | ปัดเลข |
| 17 | Object.keys() / values() / entries() | Object | ดึง key / value / คู่ |
| 18 | JSON.stringify() / JSON.parse() | JSON | แปลง object กับสตริง |
| 19 | Template literal | String | แทรกตัวแปรในสตริง |
| 20 | array.slice() | Array | ตัดช่วงสมาชิก |
| 21 | string.startsWith() / endsWith() | String | ตรวจต้น / ท้ายสตริง |
| 22 | string.toLowerCase() / toUpperCase() | String | เปลี่ยนตัวพิมพ์ |
| 23 | array.some() / every() | Array | มีบางตัว / ทุกตัวตรงเงื่อนไข |
| 24 | Promise.all() | Promise | รอหลาย async พร้อมกัน |
| 25 | ?. และ ?? | Operator | ป้องกัน null / ค่าเริ่มต้น |
รายละเอียดตัวอย่างและผลลัพธ์ของ TypeScript อยู่ในหัวข้อ 19
17. Matcher ของ expect ทั้งหมด (เติมส่วนที่ขาด)
แบ่ง 3 กลุ่ม: (A) ค่าทั่วไป ไม่ retry, (B) Locator / Page แบบ web-first retry, (C) ตัวช่วยเสริม
17.1 กลุ่ม A: ค่าทั่วไป (เรียงตามความนิยม)
expect(5).toBe(5); // เทียบแบบ Object.is (ชนิดพื้นฐาน)
expect({ a: 1 }).toEqual({ a: 1 }); // เทียบเนื้อใน object / array
expect(x).toBeTruthy(); // truthy
expect(x).toBeFalsy(); // falsy: 0, '', null, undefined, false, NaN
expect(arr).toHaveLength(3);
expect(arr).toContain('a'); // array มีค่านี้
expect(text).toContain('Thank'); // สตริงมีข้อความนี้
expect(text).toMatch(/thank/i);
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(5).toBeLessThanOrEqual(5);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // เลขทศนิยม
expect(obj).toMatchObject({ id: 1 }); // มี property เหล่านี้ (ที่เหลือไม่สน)
expect(obj).toHaveProperty('user.name', 'Somchai');
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(0).toBeDefined(); // ไม่ใช่ undefined (null, 0, '' ผ่าน)
expect(NaN).toBeNaN();
expect({ a: 1, b: undefined }).toStrictEqual({ a: 1 }); // fail: เข้มกว่า toEqual
expect(arr).toContainEqual({ id: 1 }); // array มี object ที่เนื้อเท่ากัน
expect(() => fn()).toThrow('error msg');
expect(new Date()).toBeInstanceOf(Date);ความต่างที่มักสับสน:
| Matcher | ผลลัพธ์ตัวอย่าง |
|---|---|
expect({a:1}).toBe({a:1}) | fail เพราะคนละ reference |
expect({a:1}).toEqual({a:1}) | ผ่าน |
expect(null).toBeDefined() | ผ่าน (null ไม่ใช่ undefined) |
expect(null).toBeFalsy() | ผ่าน |
expect([]).toBeTruthy() | ผ่าน (array ว่างยัง truthy) |
ข้อความเมื่อ fail:
Error: expect(received).toBe(expected) // Object.is equality
Expected: 5
Received: 4Error: expect(received).toBeGreaterThanOrEqual(expected)
Expected: >= 49.99
Received: 29.99Error: expect(received).toContain(expected) // indexOf
Expected value: "Thank"
Received string: "Checkout: Overview"17.2 Asymmetric matcher (ใช้ซ้อนใน toEqual / toMatchObject)
expect(user).toEqual({
id: expect.any(Number), // ชนิดตรง ค่าอะไรก็ได้
name: expect.stringContaining('Som'),
email: expect.stringMatching(/@.+/),
tags: expect.arrayContaining(['qa']),
meta: expect.objectContaining({ ok: true }),
token: expect.anything(), // ไม่ใช่ null / undefined
});17.3 กลุ่ม B: Locator / Page matcher (retry อัตโนมัติ)
| Matcher | ตรวจอะไร | ตัวอย่าง |
|---|---|---|
toBeVisible() ⭐⭐⭐ | แสดงอยู่ | await expect(loc).toBeVisible() |
toHaveText() ⭐⭐⭐ | ข้อความตรงทั้งหมด | toHaveText('Products') |
toContainText() ⭐⭐⭐ | ข้อความมีบางส่วน | toContainText('Thank') |
toHaveURL() ⭐⭐⭐ | URL ของ page | toHaveURL(/inventory/) |
toHaveCount() ⭐⭐⭐ | จำนวน element | toHaveCount(6) |
toHaveValue() ⭐⭐ | ค่าใน input | toHaveValue('abc') |
toBeEnabled() / toBeDisabled() ⭐⭐ | เปิด / ปิดใช้งาน | toBeDisabled() |
toBeChecked() ⭐⭐ | ติ๊กอยู่ | toBeChecked({ checked: false }) |
toHaveAttribute() ⭐⭐ | attribute | toHaveAttribute('href', '/cart.html') |
toHaveTitle() ⭐⭐ | title ของ page | toHaveTitle('Swag Labs') |
toBeHidden() ⭐⭐ | ซ่อน / ไม่มีใน DOM | toBeHidden() |
toHaveClass() ⭐ | class | toHaveClass(/active/) |
toHaveCSS() ⭐ | สไตล์ | toHaveCSS('color', 'rgb(0, 0, 0)') |
toBeEmpty() ⭐ | ว่าง | toBeEmpty() |
toBeFocused() ⭐ | ถูก focus | toBeFocused() |
toBeEditable() ⭐ | แก้ไขได้ | toBeEditable() |
toBeAttached() ⭐ | อยู่ใน DOM | toBeAttached() |
toBeInViewport() ⭐ | อยู่ในหน้าจอ | toBeInViewport({ ratio: 0.5 }) |
toHaveId() ⭐ | id | toHaveId('main') |
toHaveValues() ⭐ | หลายค่าใน multi-select | toHaveValues(['a', 'b']) |
toHaveJSProperty() ⭐ | JS property | toHaveJSProperty('loop', true) |
toHaveAccessibleName() ⭐ | accessible name | toHaveAccessibleName('Login') |
toHaveAccessibleDescription() ⭐ | accessible description | toHaveAccessibleDescription('...') |
toHaveRole() ⭐ | ARIA role | toHaveRole('button') |
toContainClass() ⭐ | มี class นั้นอยู่ | toContainClass('btn') |
toHaveScreenshot() ⭐ | เทียบภาพ | toHaveScreenshot('a.png') |
toMatchAriaSnapshot() ⭐ | เทียบ accessibility tree | toMatchAriaSnapshot(...) |
toBeOK() ⭐⭐ | API response 2xx | await expect(res).toBeOK() |
ตัวอย่างผลลัพธ์เมื่อ fail:
Error: expect(locator).toHaveCount(expected)
Locator: locator('.inventory_item')
Expected: 6
Received: 5
Timeout: 5000msError: expect(page).toHaveURL(expected)
Expected string: "https://www.saucedemo.com/checkout-complete.html"
Received string: "https://www.saucedemo.com/checkout-step-two.html"17.4 กลุ่ม C: ตัวช่วยเสริมของ expect
| ตัวช่วย | ใช้ทำอะไร |
|---|---|
expect(...).not | กลับเงื่อนไข |
expect.soft(...) | assert แล้วไม่หยุดเทสต์ถ้า fail |
expect.poll(fn) | เรียก fn ซ้ำจนผ่าน (ใช้กับค่า async) |
expect(fn).toPass() | รันบล็อกซ้ำจนไม่ throw |
expect.configure({ timeout, soft }) | สร้าง expect เวอร์ชันตั้งค่าเอง |
expect.extend({...}) | สร้าง matcher ใหม่เอง |
expect(value, 'ข้อความ') | ใส่ข้อความอธิบายตอน fail |
await expect(async () => {
const res = await page.request.get('/api/status');
expect(res.status()).toBe(200);
}).toPass({ intervals: [500, 1000], timeout: 10_000 });
const slowExpect = expect.configure({ timeout: 15_000 });
await slowExpect(page.getByText('Done')).toBeVisible();
expect.extend({
toBePositive(received: number) {
const pass = received > 0;
return { pass, message: () => `expected ${received} to be positive` };
},
});18. Playwright API ทั้งหมด (ดัชนีฉบับเต็ม)
เรียงในแต่ละตารางจากใช้บ่อยไปน้อย ฟังก์ชันที่ไม่มีดาวคือใช้เฉพาะงานเฉพาะทาง รายละเอียดตัวอย่างของตัวที่ใช้บ่อยอยู่ในหัวข้อ 1 ถึง 15 ตัวอย่างสั้นในตารางนี้ใส่ผลลัพธ์ไว้ในวงเล็บ
18.1 Page
| เมธอด | ใช้ทำอะไร | ตัวอย่าง (ผลลัพธ์) |
|---|---|---|
goto(url) ⭐⭐⭐ | เปิด URL | await page.goto('/') (คืน Response) |
locator(sel) ⭐⭐⭐ | สร้าง Locator | page.locator('.item') |
getByRole/Text/Label/Placeholder/TestId/AltText/Title ⭐⭐⭐ | หา element แบบ semantic | page.getByRole('button') |
url() ⭐⭐⭐ | URL ปัจจุบัน | page.url() ('https://.../inventory.html') |
title() ⭐⭐ | title ของหน้า | await page.title() ('Swag Labs') |
waitForURL(url) ⭐⭐ | รอ URL เปลี่ยน | waitForURL('**/cart.html') |
waitForLoadState(s) ⭐⭐ | รอสถานะโหลด | 'networkidle' |
waitForResponse(pred) ⭐⭐ | รอ response | ดูหัวข้อ 6.3 |
waitForRequest(pred) ⭐⭐ | รอ request | waitForRequest('**/api/**') |
waitForEvent(name) ⭐⭐ | รอ event | 'popup', 'download', 'dialog' |
waitForFunction(fn) ⭐ | รอเงื่อนไขใน browser | waitForFunction(() => window.ready) |
waitForTimeout(ms) ⭐ | หน่วงเวลา (ใช้ debug) | waitForTimeout(1000) |
reload() ⭐⭐ | โหลดหน้าใหม่ | await page.reload() |
goBack() / goForward() ⭐ | ย้อน / ไปข้างหน้า | await page.goBack() |
screenshot(opts) ⭐⭐ | ถ่ายภาพ | screenshot({ path: 'a.png' }) |
evaluate(fn) ⭐⭐ | รัน JS ใน browser | evaluate(() => document.title) ('Swag Labs') |
evaluateHandle(fn) ⭐ | รัน JS คืน handle | evaluateHandle(() => document.body) |
content() ⭐ | HTML ทั้งหน้า | await page.content() (string) |
setContent(html) ⭐ | ใส่ HTML เอง | setContent('<h1>Hi</h1>') |
route(url, fn) ⭐⭐ | ดัก / mock request | ดูหัวข้อ 10 |
unroute(url) ⭐ | เลิกดัก | unroute('**/api/**') |
routeFromHAR(file) ⭐ | เล่นซ้ำจากไฟล์ HAR | routeFromHAR('a.har') |
on(event, fn) / once / off ⭐⭐ | ฟัง event | page.on('console', ...) |
frameLocator(sel) ⭐⭐ | เข้า iframe | frameLocator('#f') |
frame(name) / frames() / mainFrame() ⭐ | จัดการ Frame | page.frames().length (2) |
pause() ⭐⭐ | เปิด Inspector | await page.pause() |
close() ⭐⭐ | ปิดหน้า | await page.close() |
isClosed() ⭐ | ปิดแล้วหรือยัง | page.isClosed() (false) |
setViewportSize(size) ⭐⭐ | ปรับขนาดหน้าต่าง | setViewportSize({ width: 390, height: 844 }) |
viewportSize() ⭐ | ขนาดปัจจุบัน | ({ width: 1280, height: 720 }) |
setDefaultTimeout(ms) ⭐ | timeout ของ action | setDefaultTimeout(10000) |
setDefaultNavigationTimeout(ms) ⭐ | timeout ตอน navigate | setDefaultNavigationTimeout(60000) |
setExtraHTTPHeaders(h) ⭐ | เพิ่ม header ทุก request | { 'x-env': 'test' } |
addInitScript(fn) ⭐ | สคริปต์รันก่อนโหลดหน้า | addInitScript(() => {...}) |
addScriptTag() / addStyleTag() ⭐ | แทรก script / CSS | addStyleTag({ content: 'a{color:red}' }) |
exposeFunction(name, fn) ⭐ | เปิดฟังก์ชัน Node ให้หน้าเว็บเรียก | exposeFunction('sha', fn) |
emulateMedia(opts) ⭐ | จำลอง print / dark mode | emulateMedia({ colorScheme: 'dark' }) |
dragAndDrop(src, dst) ⭐ | ลากวาง | dragAndDrop('#a', '#b') |
pdf(opts) ⭐ | สร้าง PDF (Chromium) | pdf({ path: 'a.pdf' }) |
bringToFront() ⭐ | เอาแท็บมาหน้าสุด | await page.bringToFront() |
context() ⭐ | BrowserContext ของหน้านี้ | page.context() |
opener() ⭐ | หน้าที่เปิดหน้านี้ | await popup.opener() |
video() ⭐ | อ็อบเจกต์วิดีโอ | await page.video()?.path() |
workers() ⭐ | Web Worker | page.workers().length |
request ⭐⭐ | APIRequestContext (ใช้ session เดียวกัน) | page.request.get('/api') |
keyboard / mouse / touchscreen ⭐ | อุปกรณ์ input | ดูหัวข้อ 18.5 |
clock ⭐ | คุมเวลาปลอม | await page.clock.install() |
Event ของ page: close, console, crash, dialog, domcontentloaded, download, filechooser, frameattached, framedetached, framenavigated, load, pageerror, popup, request, requestfailed, requestfinished, response, websocket, worker
18.2 Locator
| เมธอด | ใช้ทำอะไร | ตัวอย่าง (ผลลัพธ์) |
|---|---|---|
click() ⭐⭐⭐ | คลิก | await loc.click() |
fill(v) ⭐⭐⭐ | กรอกข้อความ | await loc.fill('abc') |
locator(sel) ⭐⭐⭐ | ค้นต่อภายใน | loc.locator('button') |
getBy... ⭐⭐⭐ | หาต่อภายใน | loc.getByText('x') |
first() / last() / nth(i) ⭐⭐⭐ | เลือกตำแหน่ง | loc.nth(2) |
count() ⭐⭐⭐ | จำนวน | (6) |
allTextContents() ⭐⭐⭐ | ข้อความทุกตัว | (['$29.99', '$9.99']) |
textContent() ⭐⭐ | ข้อความ (รวมที่ซ่อน) | ('Backpack') |
innerText() ⭐⭐ | ข้อความที่มองเห็น | ('Backpack') |
filter(opts) ⭐⭐ | กรอง | filter({ hasText: 'Bike' }) |
selectOption(v) ⭐⭐ | เลือก dropdown | (['hilo']) |
check() / uncheck() / setChecked(b) ⭐⭐ | ติ๊ก | await loc.check() |
isVisible() ⭐⭐ | boolean ทันที | (true) |
isEnabled() / isDisabled() ⭐⭐ | เปิด / ปิดใช้งาน | (false) |
isChecked() ⭐⭐ | ติ๊กอยู่ไหม | (true) |
isHidden() / isEditable() ⭐ | ซ่อน / แก้ไขได้ | (false) |
getAttribute(n) ⭐⭐ | ค่า attribute | ('/cart.html') |
inputValue() ⭐⭐ | ค่าใน input | ('standard_user') |
press(key) ⭐⭐ | กดแป้น | press('Enter') |
pressSequentially(t) ⭐⭐ | พิมพ์ทีละตัว | pressSequentially('abc', { delay: 50 }) |
hover() ⭐⭐ | วางเมาส์ | await loc.hover() |
waitFor(opts) ⭐⭐ | รอสถานะ | waitFor({ state: 'hidden' }) |
all() ⭐⭐ | array ของ Locator | for (const l of await loc.all()) |
clear() ⭐⭐ | ล้างค่า | await loc.clear() |
dblclick() ⭐ | ดับเบิลคลิก | await loc.dblclick() |
focus() / blur() ⭐ | focus / เลิก focus | await loc.focus() |
setInputFiles(f) ⭐ | อัปโหลดไฟล์ | setInputFiles('a.png') |
dragTo(target) ⭐ | ลากวาง | dragTo(other) |
screenshot() ⭐ | ถ่ายภาพ element | screenshot({ path: 'a.png' }) |
scrollIntoViewIfNeeded() ⭐ | เลื่อนหา | await loc.scrollIntoViewIfNeeded() |
boundingBox() ⭐ | ตำแหน่งและขนาด | ({ x, y, width, height }) |
evaluate(fn) / evaluateAll(fn) ⭐ | รัน JS | evaluateAll(els => els.length) |
evaluateHandle(fn) ⭐ | รัน JS คืน handle | loc.evaluateHandle(el => el) |
allInnerTexts() ⭐ | innerText ทุกตัว | (['a', 'b']) |
innerHTML() ⭐ | HTML ภายใน | ('<span>x</span>') |
selectText() ⭐ | เลือกข้อความ | await loc.selectText() |
tap() ⭐ | แตะ (touch) | await loc.tap() |
dispatchEvent(type) ⭐ | ยิง DOM event | dispatchEvent('click') |
and(l) / or(l) ⭐ | รวม locator | a.or(b) |
highlight() ⭐ | ไฮไลต์ตอน debug | await loc.highlight() |
ariaSnapshot() ⭐ | ภาพ accessibility tree (YAML) | await loc.ariaSnapshot() |
contentFrame() ⭐ | แปลงเป็น FrameLocator | loc.contentFrame() |
page() ⭐ | Page ของ locator | loc.page() |
describe(text) ⭐ | ตั้งชื่ออ่านง่ายในรายงาน | loc.describe('ปุ่ม Login') |
18.3 BrowserContext และ Browser
| เมธอด | ใช้ทำอะไร |
|---|---|
browser.newContext(opts) ⭐⭐ | สร้าง context (session แยก) |
context.newPage() ⭐⭐ | เปิดแท็บใหม่ |
context.pages() ⭐⭐ | แท็บทั้งหมด |
context.storageState({ path }) ⭐⭐ | บันทึก cookies + storage |
context.addCookies([...]) / cookies() / clearCookies() ⭐⭐ | จัดการ cookies |
context.waitForEvent('page') ⭐⭐ | รอแท็บใหม่ |
context.route() / unroute() ⭐ | ดัก request ทั้ง context |
context.grantPermissions([...]) / clearPermissions() ⭐ | สิทธิ์ เช่น geolocation |
context.setGeolocation(g) ⭐ | ตั้งพิกัด |
context.setOffline(b) ⭐ | จำลองออฟไลน์ |
context.setExtraHTTPHeaders(h) ⭐ | header ทุก request |
context.setDefaultTimeout(ms) ⭐ | timeout ทั้ง context |
context.addInitScript(fn) ⭐ | สคริปต์ก่อนโหลดทุกหน้า |
context.exposeFunction() / exposeBinding() ⭐ | เปิดฟังก์ชัน Node |
context.tracing.start() / stop() ⭐⭐ | บันทึก trace |
context.request ⭐ | APIRequestContext |
context.newCDPSession(page) ⭐ | Chrome DevTools Protocol |
context.close() ⭐⭐ | ปิด context |
browser.close() ⭐⭐ | ปิด browser |
browser.newPage() ⭐ | ทางลัด (สร้าง context + page) |
browser.contexts() ⭐ | context ทั้งหมด |
browser.version() ⭐ | เวอร์ชัน ('131.0.6778.33') |
browser.isConnected() ⭐ | ยังเชื่อมต่อไหม |
chromium.launch(opts) ⭐⭐ | เปิด browser |
chromium.launchPersistentContext(dir) ⭐ | โปรไฟล์ถาวร |
chromium.connectOverCDP(url) ⭐ | ต่อกับ Chrome ที่เปิดอยู่ |
chromium.connect(wsEndpoint) ⭐ | ต่อกับ Playwright server |
chromium.executablePath() ⭐ | path ของ browser |
18.4 Request, Response, Route, WebSocket
| อ็อบเจกต์ | เมธอด |
|---|---|
Request | url(), method(), headers(), postData(), postDataJSON(), resourceType(), response(), failure(), frame(), redirectedFrom(), redirectedTo(), isNavigationRequest(), timing(), allHeaders(), headerValue(name) |
Response | ok(), status(), statusText(), url(), headers(), json(), text(), body(), request(), frame(), finished(), allHeaders(), headerValue(name), serverAddr(), securityDetails() |
Route | fulfill(), continue(), abort(), fallback(), fetch(), request() |
WebSocket | url(), isClosed(), waitForEvent('framereceived'), events: framesent, framereceived, close, socketerror |
page.on('response', async res => {
if (res.url().includes('/api/')) {
console.log(res.status(), res.url());
console.log(await res.json());
}
});ผลลัพธ์:
200 https://example.com/api/products
[ { id: 1, name: 'Backpack' }, { id: 2, name: 'Bike Light' } ]18.5 Keyboard, Mouse, Touchscreen, Dialog, Download, FileChooser
| อ็อบเจกต์ | เมธอด |
|---|---|
page.keyboard | press(key), down(key), up(key), type(text), insertText(text) |
page.mouse | move(x, y), click(x, y), dblclick(x, y), down(), up(), wheel(dx, dy) |
page.touchscreen | tap(x, y) |
Dialog | accept(text?), dismiss(), message(), type(), defaultValue() |
Download | saveAs(path), path(), suggestedFilename(), url(), failure(), cancel(), delete() |
FileChooser | setFiles(files), element(), isMultiple() |
18.6 APIRequestContext และ APIResponse
| เมธอด | ใช้ทำอะไร |
|---|---|
request.get(url, opts) ⭐⭐ | GET |
request.post(url, { data }) ⭐⭐ | POST |
request.put() / patch() / delete() / head() ⭐ | เมธอดอื่น |
request.fetch(url, { method }) ⭐ | เมธอดกำหนดเอง |
playwright.request.newContext({ baseURL, extraHTTPHeaders }) ⭐⭐ | สร้าง API client แยก |
request.storageState() / dispose() ⭐ | บันทึก session / ปิด |
res.ok() / status() ⭐⭐ | สถานะ |
res.json() / text() / body() ⭐⭐ | อ่านเนื้อหา |
res.headers() / url() / statusText() ⭐ | ข้อมูลเสริม |
const api = await playwright.request.newContext({
baseURL: 'https://api.example.com',
extraHTTPHeaders: { Authorization: 'Bearer xxx' },
});
const res = await api.get('/users/1');
console.log(res.status(), await res.json());ผลลัพธ์:
200 { id: 1, name: 'Somchai' }18.7 Test Runner (@playwright/test)
| API | ใช้ทำอะไร |
|---|---|
test(name, fn) ⭐⭐⭐ | นิยามเทสต์ |
test.describe(name, fn) ⭐⭐⭐ | จัดกลุ่ม |
test.beforeEach / afterEach ⭐⭐⭐ | hook ต่อเทสต์ |
test.beforeAll / afterAll ⭐⭐ | hook ครั้งเดียว |
test.step(name, fn) ⭐⭐ | แบ่งขั้นตอน |
test.use(opts) ⭐⭐ | ตั้ง option ระดับไฟล์ / กลุ่ม |
test.skip() / only() / fixme() / fail() / slow() ⭐⭐ | ควบคุมการรัน |
test.setTimeout(ms) ⭐⭐ | timeout เทสต์นี้ |
test.extend({...}) ⭐⭐ | custom fixture |
test.describe.configure({ mode }) ⭐ | 'serial', 'parallel', 'default' |
test.describe.serial() / parallel() ⭐ | รันเรียง / ขนาน |
test.info() ⭐ | ข้อมูลเทสต์ปัจจุบัน |
testInfo.attach(name, opts) ⭐ | แนบไฟล์เข้ารายงาน |
testInfo.title / project.name / retry / outputPath() ⭐ | ข้อมูลเทสต์ |
test.expect / test.mergeTests() / test.mergeExpects() ⭐ | รวม fixture / expect |
defineConfig() ⭐⭐ | ครอบไฟล์ config |
devices['iPhone 13'] ⭐⭐ | โปรไฟล์อุปกรณ์ |
Fixture ในตัว: page, context, browser, browserName, request, playwright
ตัวอย่างการแนบไฟล์และอ่านข้อมูลเทสต์:
test('demo', async ({ page }, testInfo) => {
console.log(testInfo.title, testInfo.retry); // demo 0
await testInfo.attach('screenshot', {
body: await page.screenshot(),
contentType: 'image/png',
});
});19. TypeScript: ฟังก์ชันที่ใช้กับค่าของตัวแปร (เรียงตามความนิยม)
เรียงหัวข้อจากชนิดที่ใช้บ่อยสุดในงานเทสต์: Array, String, Number, Math, Object, JSON, Promise, Date ผลลัพธ์ในคอมเมนต์ // คือค่าที่ได้จริง
ข้อมูลตัวอย่างที่ใช้ทั้งหัวข้อ:
const prices = [49.99, 29.99, 15.99, 15.99, 9.99, 7.99];
const names = ['Backpack', 'Bike Light', 'Bolt T-Shirt'];19.1 Array (ชนิดที่ใช้บ่อยที่สุด)
ฟังก์ชันที่ต้องรู้ ⭐⭐⭐
prices.length; // 6
prices.map(p => p * 2); // [99.98, 59.98, 31.98, 31.98, 19.98, 15.98]
prices.filter(p => p > 15); // [49.99, 29.99, 15.99, 15.99]
prices.find(p => p < 10); // 9.99 (ตัวแรกที่เจอ ไม่เจอได้ undefined)
prices.findIndex(p => p < 10); // 4 (ไม่เจอได้ -1)
prices.includes(9.99); // true
prices.indexOf(15.99); // 2 (ไม่เจอได้ -1)
prices.some(p => p > 40); // true (มีอย่างน้อย 1 ตัว)
prices.every(p => p > 5); // true (ทุกตัวต้องผ่าน)
prices.reduce((sum, p) => sum + p, 0); // 129.94 (อาจได้ 129.94000000000003 ให้ใช้ toFixed)
names.join(', '); // 'Backpack, Bike Light, Bolt T-Shirt'
names.forEach(n => console.log(n)); // พิมพ์ทีละชื่อ (คืน undefined)เรียงและตัด ⭐⭐
[3, 1, 2].sort(); // [1, 2, 3] (ระวัง: เรียงแบบตัวอักษร)
[10, 9, 1].sort(); // [1, 10, 9] (ผิดความคาดหวัง)
[10, 9, 1].sort((a, b) => a - b); // [1, 9, 10] เรียงเลขน้อยไปมาก
[10, 9, 1].sort((a, b) => b - a); // [10, 9, 1] เรียงมากไปน้อย
[...prices].sort((a, b) => b - a); // สำเนา: ไม่แก้ array เดิม (sort แก้ array เดิม)
prices.slice(0, 2); // [49.99, 29.99] (ไม่แก้ array เดิม)
prices.slice(-2); // [9.99, 7.99]
prices.at(-1); // 7.99 (ตัวสุดท้าย)
prices[prices.length - 1]; // 7.99 (วิธีเดิม)เพิ่ม ลบ แก้ ⭐⭐ (แก้ array เดิม)
const a = [1, 2, 3];
a.push(4); // a = [1, 2, 3, 4], คืนความยาวใหม่ 4
a.pop(); // คืน 4, a = [1, 2, 3]
a.unshift(0); // เพิ่มหน้า a = [0, 1, 2, 3]
a.shift(); // คืน 0, a = [1, 2, 3]
a.splice(1, 1); // ลบ 1 ตัวที่ index 1 คืน [2], a = [1, 3]
a.reverse(); // a = [3, 1]แปลงและรวม ⭐
[1, 2].concat([3]); // [1, 2, 3]
[...[1, 2], ...[3, 4]]; // [1, 2, 3, 4] (spread)
[[1, 2], [3]].flat(); // [1, 2, 3]
names.flatMap(n => n.split(' ')); // ['Backpack', 'Bike', 'Light', 'Bolt', 'T-Shirt']
Array.from({ length: 3 }, (_, i) => i); // [0, 1, 2]
Array.isArray(prices); // true
new Set(prices); // Set { 49.99, 29.99, 15.99, 9.99, 7.99 }
[...new Set(prices)]; // [49.99, 29.99, 15.99, 9.99, 7.99] (ตัดซ้ำ)
Array(3).fill(0); // [0, 0, 0]
prices.entries(); // iterator ของ [index, value]Array ในงาน Playwright: ตัวอย่างจริง
const texts = await page.locator('.inventory_item_price').allTextContents();
// ['$49.99', '$29.99', '$15.99', '$15.99', '$9.99', '$7.99']
const nums = texts.map(t => parseFloat(t.replace('$', '')));
// [49.99, 29.99, 15.99, 15.99, 9.99, 7.99]
const sorted = [...nums].sort((a, b) => b - a);
expect(nums).toEqual(sorted); // ตรวจเรียงจากมากไปน้อย
const total = nums.reduce((s, n) => s + n, 0);
console.log(total.toFixed(2)); // '129.94'19.2 String
ที่ต้องรู้ ⭐⭐⭐
const s = ' Hello Playwright ';
s.length; // 20
s.trim(); // 'Hello Playwright'
s.trimStart(); // 'Hello Playwright '
s.trimEnd(); // ' Hello Playwright'
s.toLowerCase(); // ' hello playwright '
s.toUpperCase(); // ' HELLO PLAYWRIGHT '
s.includes('Play'); // true
s.replace('Hello', 'Hi'); // ' Hi Playwright ' (แทนตัวแรกตัวเดียว)
s.replaceAll('l', 'L'); // ' HeLLo PLaywright '
s.split(' '); // ['', '', 'Hello', 'Playwright', '', '']
s.trim().split(' '); // ['Hello', 'Playwright']
'$29.99'.replace('$', ''); // '29.99'
'$1,299.50'.replace(/[^0-9.]/g, ''); // '1299.50' (ตัดทุกอย่างที่ไม่ใช่เลข / จุด)ตรวจและค้น ⭐⭐
const u = 'https://www.saucedemo.com/inventory.html';
u.startsWith('https'); // true
u.endsWith('.html'); // true
u.indexOf('saucedemo'); // 12 (ไม่เจอได้ -1)
u.includes('cart'); // false
u.match(/inventory/); // ['inventory', index: 26, ...]
u.match(/[0-9]+/g); // null (ไม่มีตัวเลข)
'a1b22'.match(/[0-9]+/g); // ['1', '22']
/inventory/.test(u); // true (regex.test คืน boolean)ตัดและเติม ⭐⭐
'Backpack'.slice(0, 4); // 'Back'
'Backpack'.slice(-4); // 'pack'
'Backpack'.substring(4); // 'pack'
'Backpack'.charAt(0); // 'B'
'Backpack'.at(-1); // 'k'
'5'.padStart(3, '0'); // '005'
'5'.padEnd(3, '0'); // '500'
'ab'.repeat(3); // 'ababab'
'a,b,c'.split(','); // ['a', 'b', 'c']
'Bolt T-Shirt'.split(' ')[0]; // 'Bolt'Template literal ⭐⭐⭐
const user = 'standard_user';
const n = 3;
const msg = `ผู้ใช้ ${user} มีสินค้า ${n} ชิ้น รวม ${(n * 9.99).toFixed(2)} บาท`;
// 'ผู้ใช้ standard_user มีสินค้า 3 ชิ้น รวม 29.97 บาท'
await page.locator(`[data-test="add-to-cart-${slug}"]`).click();อื่น ๆ ⭐
'a'.localeCompare('b'); // -1 (ใช้เรียงสตริง: arr.sort((x, y) => x.localeCompare(y)))
'Ab'.toLowerCase() === 'ab'; // true (เทียบแบบไม่สนตัวพิมพ์)
'abc'.split('').reverse().join('');// 'cba'
[...'abc']; // ['a', 'b', 'c']
[...'a1b2'.matchAll(/[0-9]/g)].map(m => m[0]); // ['1', '2']19.3 Number และการแปลงชนิด
parseFloat('29.99'); // 29.99
parseFloat('$29.99'); // NaN <- บั๊กที่เจอในแชท ต้องตัด $ ก่อน
parseFloat('29.99abc'); // 29.99 (อ่านเท่าที่เป็นเลข)
parseInt('42px', 10); // 42
parseInt('3.9', 10); // 3
Number('29.99'); // 29.99
Number('29.99abc'); // NaN (เข้มกว่า parseFloat)
Number(''); // 0 (ระวัง)
+'12'; // 12 (ทางลัด)
Number.isNaN(NaN); // true
Number.isNaN('abc'); // false (ไม่แปลงชนิด)
isNaN('abc'); // true (แปลงชนิดก่อน ไม่แนะนำ)
Number.isInteger(5.0); // true
Number.isFinite(1 / 0); // false
(29.99).toFixed(1); // '30.0' <- ได้ string ไม่ใช่ number
Number((0.1 + 0.2).toFixed(2)); // 0.3
(1234567.891).toLocaleString(); // '1,234,567.891'
(1234.5).toLocaleString('th-TH', { style: 'currency', currency: 'THB' }); // '฿1,234.50'
(255).toString(16); // 'ff'
(5).toString(); // '5'
String(5); // '5'
Boolean(''); // false
!!'abc'; // true
Number.EPSILON; // 2.220446049250313e-16
Number.MAX_SAFE_INTEGER; // 9007199254740991ค่า falsy ทั้งหมด: false, 0, -0, 0n, '', null, undefined, NaN (นอกนั้นเป็น truthy รวมถึง '0', [], {})
ปัญหาทศนิยมที่ต้องรู้:
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false
expect(0.1 + 0.2).toBeCloseTo(0.3, 2); // ผ่าน
Math.round((0.1 + 0.2) * 100) / 100; // 0.319.4 Math
Math.max(...prices); // 49.99 (ต้อง spread: Math.max(prices) ได้ NaN)
Math.min(...prices); // 7.99
Math.round(2.5); // 3
Math.round(-2.5); // -2 (ปัดเข้าหา +infinity)
Math.floor(2.9); // 2
Math.ceil(2.1); // 3
Math.trunc(-2.9); // -2 (ตัดทศนิยมทิ้ง)
Math.abs(-5); // 5
Math.pow(2, 3); // 8 (หรือ 2 ** 3)
Math.sqrt(16); // 4
Math.random(); // 0 ถึงน้อยกว่า 1 เช่น 0.4817...
Math.floor(Math.random() * 100); // เลขสุ่ม 0 ถึง 99
Math.sign(-3); // -1
Math.PI; // 3.141592653589793สุ่มข้อมูลทดสอบ เช่น อีเมลไม่ซ้ำ:
const email = `user${Date.now()}@test.com`; // 'user1761700000000@test.com'
const rand = Math.floor(Math.random() * 9000) + 1000; // เลข 4 หลัก 1000 ถึง 999919.5 Object
const item = { name: 'Backpack', price: 29.99, tags: ['bag', 'sale'] };
Object.keys(item); // ['name', 'price', 'tags']
Object.values(item); // ['Backpack', 29.99, ['bag', 'sale']]
Object.entries(item); // [['name', 'Backpack'], ['price', 29.99], ['tags', [...]]]
Object.fromEntries([['a', 1], ['b', 2]]);// { a: 1, b: 2 }
Object.assign({}, item, { price: 19 }); // { name: 'Backpack', price: 19, tags: [...] }
Object.hasOwn(item, 'price'); // true
Object.freeze(item); // ห้ามแก้ค่า (ใน strict mode แก้แล้ว error)
structuredClone(item); // คัดลอกลึก (deep copy)
// Spread: คัดลอกตื้น และแก้บางค่า
const updated = { ...item, price: 19.99 }; // { name: 'Backpack', price: 19.99, tags: [...] }
// Destructuring
const { name, price } = item; // name = 'Backpack', price = 29.99
const { name: productName } = item; // ตั้งชื่อใหม่
const [first, second] = ['a', 'b', 'c']; // first = 'a', second = 'b'
// Optional chaining และ nullish
item.discount?.rate; // undefined (ไม่ error แม้ discount ไม่มี)
item.discount?.rate ?? 0; // 0 (ใช้ค่าเริ่มต้นเฉพาะ null / undefined)
item.price || 10; // 29.99 (|| ใช้ค่าเริ่มต้นกับ falsy ทั้งหมด รวม 0)
0 || 10; // 10
0 ?? 10; // 0
'price' in item; // true
delete (item as any).tags; // ลบ propertyใช้บ่อยในเทสต์: แปลงข้อมูลผู้ซื้อเป็นก้อนเดียวแล้ววนกรอก
const buyer = { firstName: 'Somchai', lastName: 'Jaidee', postalCode: '10110' };
for (const [key, value] of Object.entries(buyer)) {
await page.locator(`[data-test="${key}"]`).fill(value);
}19.6 JSON
const obj = { id: 1, name: 'Somchai' };
JSON.stringify(obj); // '{"id":1,"name":"Somchai"}'
JSON.stringify(obj, null, 2); // จัดย่อหน้า 2 ช่องให้อ่านง่าย
JSON.parse('{"id":1}'); // { id: 1 }
JSON.parse('not json'); // throw SyntaxError: Unexpected token 'o'
const copy = JSON.parse(JSON.stringify(obj)); // คัดลอกลึกแบบเก่า (เสียข้อมูล Date, undefined)19.7 Promise และ async / await (หัวใจของ Playwright)
await page.goto('/'); // รอจนเสร็จ ลืม await = บั๊กยอดฮิต
const [a, b] = await Promise.all([ // รันพร้อมกัน รอทั้งคู่ (เร็วกว่ารอทีละตัว)
page.locator('.a').textContent(),
page.locator('.b').textContent(),
]);
await Promise.allSettled([p1, p2]); // [{ status: 'fulfilled', value }, { status: 'rejected', reason }]
await Promise.race([p1, p2]); // เอาตัวที่เสร็จก่อน
await Promise.resolve(5); // 5
// try / catch / finally
try {
await page.locator('.x').click({ timeout: 2000 });
} catch (e) {
console.log((e as Error).message); // Timeout 2000ms exceeded.
} finally {
console.log('จบ');
}
// หน่วงเวลา (ใช้น้อยมาก)
await new Promise(r => setTimeout(r, 1000));ตัวอย่าง Promise.all กับ popup:
const [popup] = await Promise.all([
page.waitForEvent('popup'), // เริ่มรอก่อน
page.getByText('Open').click(), // แล้วค่อยคลิก (กัน popup มาก่อนที่จะรอ)
]);19.8 Date
const now = new Date();
Date.now(); // 1761700000000 (ms นับจาก 1970)
now.toISOString(); // '2026-10-29T08:30:00.000Z'
now.getFullYear(); // 2026
now.getMonth(); // 9 (เดือนเริ่มที่ 0: 9 = ตุลาคม)
now.getDate(); // 29
now.getDay(); // 4 (0 = อาทิตย์, 4 = พฤหัสบดี)
now.getHours(); // 15
now.toLocaleDateString('th-TH'); // '29/10/2569'
now.toLocaleDateString('en-GB'); // '29/10/2026'
new Date('2026-12-31'); // วันที่จากสตริง
new Date(2026, 0, 15); // 15 ม.ค. 2026 (เดือนเริ่มที่ 0)
// ความต่างของวัน
const diffDays = (new Date('2026-12-31').getTime() - now.getTime()) / 86_400_000;
// เพิ่มวัน
const next = new Date(now);
next.setDate(next.getDate() + 7); // อีก 7 วัน19.9 RegExp
const re = /^[0-9]{5}$/;
re.test('10110'); // true
re.test('1011'); // false
'a1b22'.replace(/[0-9]+/g, '#'); // 'a#b#'
'2026-10-29'.match(/([0-9]{4})-([0-9]{2})-([0-9]{2})/);
// ['2026-10-29', '2026', '10', '29', index: 0, ...]
const m = 'Total: $32.39'.match(/Total: [$]([0-9.]+)/);
Number(m?.[1]); // 32.39
new RegExp(`^${prefix}-[0-9]+$`); // สร้างจากตัวแปรธงที่ใช้บ่อย: i ไม่สนตัวพิมพ์, g ทั้งหมด, m หลายบรรทัด
19.10 Map และ Set
const set = new Set([1, 2, 2, 3]); // Set { 1, 2, 3 }
set.add(4); set.has(2); set.delete(1); // true สำหรับ has
set.size; // 3
const map = new Map<string, number>([['Backpack', 29.99]]);
map.set('Bike Light', 9.99);
map.get('Backpack'); // 29.99
map.has('X'); // false
map.size; // 2
[...map.entries()]; // [['Backpack', 29.99], ['Bike Light', 9.99]]
for (const [k, v] of map) console.log(k, v);19.11 Loop และเงื่อนไข
for (const p of prices) {} // วนค่า (ใช้ await ภายในได้)
for (const i in prices) {} // วน index (เป็น string) ไม่แนะนำกับ array
for (let i = 0; i < prices.length - 1; i++) {} // วนแบบมี index (ระวัง off-by-one)
prices.forEach(async p => { await x(p); }); // ห้าม! ไม่รอ await ให้ใช้ for...of แทน
const grade = score >= 80 ? 'A' : 'B'; // ternary
switch (browserName) { case 'webkit': break; default: break; }19.12 Operator ที่ควรรู้
| Operator | ความหมาย | ตัวอย่าง (ผลลัพธ์) |
|---|---|---|
=== / !== | เท่ากัน / ไม่เท่ากัน (เข้ม) | 1 === '1' (false) |
== | เทียบแบบแปลงชนิด (หลีกเลี่ยง) | 1 == '1' (true) |
?. | optional chaining | a?.b (undefined ถ้า a ว่าง) |
?? | nullish coalescing | null ?? 5 (5) |
|| และ && | หรือ / และ | 0 || 7 (7) |
... | spread / rest | [...a, ...b] |
! และ !! | not / แปลงเป็น boolean | !!'a' (true) |
typeof | ชนิดของค่า | typeof 5 ('number') |
instanceof | เป็น instance ของ class | x instanceof Date |
as | บอกชนิดให้ TypeScript | el as HTMLInputElement |
! ท้ายตัวแปร | บอกว่าไม่ null | value!.length |
** | ยกกำลัง | 2 ** 3 (8) |
19.13 ฟีเจอร์ชนิดข้อมูลของ TypeScript
// ชนิดพื้นฐาน
let n: number = 5;
let s: string = 'a';
let b: boolean = true;
let arr: number[] = [1, 2];
let tup: [string, number] = ['a', 1];
let any1: any; // ปิดการตรวจชนิด (หลีกเลี่ยง)
let unk: unknown; // ต้องตรวจชนิดก่อนใช้ (ปลอดภัยกว่า any)
// Union และ literal
let status: 'ok' | 'fail' = 'ok';
let id: string | number = 5;
// interface / type
interface Product { name: string; price: number; tags?: string[]; }
type Buyer = { firstName: string; lastName: string; postalCode: string };
// enum
enum Sort { AZ = 'az', ZA = 'za', LOHI = 'lohi', HILO = 'hilo' }
await select.selectOption(Sort.HILO);
// Generics และ ฟังก์ชัน
function first<T>(a: T[]): T | undefined { return a[0]; }
const toNumber = (t: string): number => parseFloat(t.replace(/[^0-9.]/g, ''));
// Utility types
type P1 = Partial<Product>; // ทุก field เป็น optional
type P2 = Required<Product>; // ทุก field บังคับ
type P3 = Pick<Product, 'name'>; // เลือกเฉพาะ field
type P4 = Omit<Product, 'tags'>; // ตัด field ออก
type P5 = Record<string, number>; // { [key: string]: number }
type P6 = Readonly<Product>; // แก้ค่าไม่ได้
type P7 = ReturnType<typeof toNumber>; // number
// Type guard
if (typeof id === 'string') { id.toUpperCase(); }ตัวอย่างใช้ interface กับข้อมูลเทสต์:
interface CartItem { name: string; price: number; }
const cart: CartItem[] = [{ name: 'Backpack', price: 29.99 }, { name: 'Bike Light', price: 9.99 }];
const total = cart.reduce((s, i) => s + i.price, 0); // 39.98
const tax = +(total * 0.08).toFixed(2); // 3.2
const grand = +(total + tax).toFixed(2); // 43.1819.14 สรุปความต่างที่พลาดบ่อย
| กรณี | ผิด | ถูก |
|---|---|---|
| แปลงราคา | Number('$9.99') (NaN) | parseFloat(t.replace('$', '')) |
| loop เทียบตัวถัดไป | i < arr.length | i < arr.length - 1 |
| max จาก array | Math.max(arr) (NaN) | Math.max(...arr) |
| เรียงเลข | arr.sort() | arr.sort((a, b) => a - b) |
| เรียงแล้วไม่อยากแก้ต้นฉบับ | arr.sort() | [...arr].sort(...) |
toFixed ได้ string | total.toFixed(2) + 1 ('32.391') | +total.toFixed(2) + 1 |
| async ใน forEach | arr.forEach(async x => await f(x)) | for (const x of arr) await f(x) |
| เทียบ object | expect(a).toBe(b) | expect(a).toEqual(b) |
| ลืม await | page.click() | await page.click() |
ใช้ || กับเลข 0 | n || 10 | n ?? 10 |
19.15 ตัวอย่างรวม: คำนวณภาษีตามโจทย์ saucedemo
const toNumber = (t: string) => parseFloat(t.replace(/[^0-9.]/g, ''));
const itemTotal = toNumber(await page.locator('[data-test="subtotal-label"]').innerText());
// 'Item total: $59.98' -> 59.98
const tax = toNumber(await page.locator('[data-test="tax-label"]').innerText());
// 'Tax: $4.80' -> 4.8
const total = toNumber(await page.locator('[data-test="total-label"]').innerText());
// 'Total: $64.78' -> 64.78
expect(tax).toBeCloseTo(+(itemTotal * 0.08).toFixed(2), 2); // 59.98 x 0.08 = 4.7984 -> 4.80
expect(total).toBeCloseTo(itemTotal + tax, 2); // 59.98 + 4.80 = 64.78เอกสารทางการ: https://playwright.dev/docs/api/class-playwright และ https://www.typescriptlang.org/docs/
20. อ่านข้อมูลจาก Excel (Data-driven Testing)
Playwright ไม่มีฟังก์ชันอ่าน Excel ในตัว ต้องใช้ไลบรารีเสริมของ Node.js ตัวเลือกเรียงตามความนิยม:
| ไลบรารี | ความนิยม | จุดเด่น | ติดตั้ง |
|---|---|---|---|
xlsx (SheetJS) | ⭐⭐⭐ | อ่านแบบ sync โค้ดสั้น แปลงเป็น JSON ได้ทันที รองรับ .xlsx .xls .csv | npm i xlsx |
exceljs | ⭐⭐⭐ | อ่านและเขียนละเอียด (สี, สูตร, สไตล์), ใช้ async | npm i exceljs |
csv-parse | ⭐⭐ | เฉพาะไฟล์ .csv เบาและเร็ว | npm i csv-parse |
papaparse | ⭐ | CSV ใช้ได้ทั้งฝั่ง Node และ browser | npm i papaparse |
หมายเหตุ: แพ็กเกจ xlsx บน npm ถูกอัปเดตล่าสุดที่เวอร์ชัน 0.18.5 และมีประกาศช่องโหว่ด้านความปลอดภัย ถ้าเป็นโปรเจกต์จริงควรติดตั้งเวอร์ชันล่าสุดจาก https://cdn.sheetjs.com ตามเอกสารของ SheetJS หรือเลือกใช้ exceljs ส่วนไฟล์ที่เชื่อถือได้ (ไฟล์เทสต์ที่ทีมสร้างเอง) ความเสี่ยงต่ำ
ไฟล์ตัวอย่าง tests/data/users.xlsx (Sheet ชื่อ Login):
| username | password | expected |
|---|---|---|
| standard_user | secret_sauce | success |
| locked_out_user | secret_sauce | error |
| problem_user | secret_sauce | success |
20.1 SheetJS (xlsx): ฟังก์ชันที่ใช้บ่อย
ติดตั้ง
npm install xlsx
npm install --save-dev @types/xlsx
npm install --save-dev @types/node
import * as XLSX from 'xlsx';
import path from 'path';
const file = path.join(process.cwd(), 'tests', 'data', 'users.xlsx');
const wb = XLSX.readFile(file); // ⭐⭐⭐ อ่านไฟล์ (sync)
console.log(wb.SheetNames); // ['Login']
const ws = wb.Sheets['Login']; // ⭐⭐⭐ เลือก sheet ตามชื่อ
const ws2 = wb.Sheets[wb.SheetNames[0]]; // ⭐⭐⭐ เลือก sheet แรกแปลง sheet เป็นข้อมูล ⭐⭐⭐
// 1) เป็น array ของ object (แถวแรกเป็นชื่อ key)
const rows = XLSX.utils.sheet_to_json(ws);
console.log(rows);ผลลัพธ์:
[
{ username: 'standard_user', password: 'secret_sauce', expected: 'success' },
{ username: 'locked_out_user', password: 'secret_sauce', expected: 'error' },
{ username: 'problem_user', password: 'secret_sauce', expected: 'success' }
]// 2) เป็น array 2 มิติ (รวมแถวหัวตาราง)
const grid = XLSX.utils.sheet_to_json<string[]>(ws, { header: 1 });
console.log(grid);ผลลัพธ์:
[
['username', 'password', 'expected'],
['standard_user', 'secret_sauce', 'success'],
['locked_out_user', 'secret_sauce', 'error'],
['problem_user', 'secret_sauce', 'success']
]ตัวเลือกของ sheet_to_json ⭐⭐
XLSX.utils.sheet_to_json(ws, {
header: 1, // 1 = array 2 มิติ, 'A' = ใช้ตัวอักษรคอลัมน์เป็น key, หรือ ['a','b','c'] ตั้งชื่อเอง
defval: '', // ค่าเริ่มต้นของเซลล์ว่าง (ไม่ตั้ง = ช่องว่างจะหายไปจาก object)
range: 1, // เริ่มอ่านจากแถวที่ index 1 (ข้ามแถวหัวข้อ) หรือ 'A2:C10'
raw: false, // false = ใช้ข้อความตามที่แสดงใน Excel (เช่น '29.99%' แทน 0.2999)
blankrows: false, // ข้ามแถวว่าง
});เซลล์ว่างโดยไม่ตั้ง defval:
{ username: 'standard_user', expected: 'success' } // password หายไปทั้ง key
{ username: 'standard_user', password: '', expected: 'success' } // เมื่อตั้ง defval: ''แปลงเป็นรูปแบบอื่น ⭐
XLSX.utils.sheet_to_csv(ws); // 'username,password,expected\nstandard_user,...'
XLSX.utils.sheet_to_html(ws); // '<table>...</table>'
XLSX.utils.sheet_to_txt(ws); // แยกด้วย tab
XLSX.utils.sheet_to_formulae(ws); // ['A1="username"', 'B1="password"', ...]อ่านเซลล์เดียว ⭐⭐
const cell = ws['B2'];
console.log(cell.v); // 'secret_sauce' ค่าดิบ
console.log(cell.w); // 'secret_sauce' ข้อความที่แสดงผล (formatted)
console.log(cell.t); // 's' ชนิด: s=สตริง, n=ตัวเลข, b=boolean, d=วันที่, e=error
console.log(cell.f); // สูตร (ถ้ามี) เช่น 'SUM(A1:A3)'
console.log(ws['Z99']); // undefined (เซลล์ไม่มี ต้องเช็กก่อนอ่าน .v)
console.log(ws['!ref']); // 'A1:C4' ช่วงข้อมูลทั้งหมดนับจำนวนแถวและคอลัมน์ ⭐
const range = XLSX.utils.decode_range(ws['!ref']!);
console.log(range.e.r + 1); // 4 จำนวนแถว (รวมหัวตาราง)
console.log(range.e.c + 1); // 3 จำนวนคอลัมน์
XLSX.utils.encode_cell({ r: 1, c: 1 }); // 'B2'
XLSX.utils.encode_col(2); // 'C'อ่านวันที่ ⭐⭐ (จุดที่พลาดบ่อย)
const wb2 = XLSX.readFile(file, { cellDates: true }); // แปลงเซลล์วันที่เป็น Date
const data = XLSX.utils.sheet_to_json(wb2.Sheets['Orders']);
// { orderDate: 2026-10-29T00:00:00.000Z } ถ้าไม่ใส่ cellDates จะได้เลข serial เช่น 46324อ่านจาก Buffer ⭐
import fs from 'fs';
const wb3 = XLSX.read(fs.readFileSync(file), { type: 'buffer' });อ่านทุก sheet ⭐
const all: Record<string, unknown[]> = {};
for (const name of wb.SheetNames) {
all[name] = XLSX.utils.sheet_to_json(wb.Sheets[name]);
}
console.log(Object.keys(all)); // ['Login', 'Checkout']เขียนไฟล์ ⭐
const out = XLSX.utils.book_new();
const sheet = XLSX.utils.json_to_sheet([{ test: 'login', result: 'pass' }]);
XLSX.utils.book_append_sheet(out, sheet, 'Result');
XLSX.writeFile(out, 'test-results/result.xlsx');
// XLSX.utils.aoa_to_sheet([['a', 'b'], [1, 2]]) สร้างจาก array 2 มิติ20.2 ExcelJS: ฟังก์ชันที่ใช้บ่อย
ExcelJS เป็น async และนับแถว / คอลัมน์เริ่มที่ 1 (ไม่ใช่ 0)
import ExcelJS from 'exceljs';
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile('tests/data/users.xlsx'); // ⭐⭐⭐ อ่านไฟล์
const ws = wb.getWorksheet('Login'); // ⭐⭐⭐ เลือกตามชื่อ (ไม่เจอได้ undefined)
const ws1 = wb.worksheets[0]; // ⭐⭐ เลือกตามลำดับ
console.log(wb.worksheets.map(s => s.name)); // ['Login']
console.log(ws!.rowCount); // 4 จำนวนแถว
console.log(ws!.columnCount); // 3 จำนวนคอลัมน์
console.log(ws!.actualRowCount); // 4 นับเฉพาะแถวที่มีข้อมูลอ่านเซลล์ ⭐⭐⭐
ws!.getCell('B2').value; // 'secret_sauce'
ws!.getCell(2, 2).value; // 'secret_sauce' (แถว 2 คอลัมน์ 2)
ws!.getRow(2).getCell(1).value; // 'standard_user'
ws!.getCell('B2').text; // ข้อความที่แสดงผล (string เสมอ)
ws!.getCell('A1').type; // ชนิดเซลล์ (ExcelJS.ValueType)
ws!.getCell('D2').formula; // สูตร
ws!.getCell('D2').result; // ผลลัพธ์ของสูตรอ่านแถวและคอลัมน์ ⭐⭐
console.log(ws!.getRow(1).values);
// [ <1 empty item>, 'username', 'password', 'expected' ] index 0 ว่างเสมอ เพราะนับจาก 1
console.log(ws!.getColumn(1).values);
// [ <1 empty item>, 'username', 'standard_user', 'locked_out_user', 'problem_user' ]วนอ่านทุกแถว ⭐⭐⭐
type User = { username: string; password: string; expected: string };
const users: User[] = [];
ws!.eachRow((row, rowNumber) => {
if (rowNumber === 1) return; // ข้ามหัวตาราง
users.push({
username: String(row.getCell(1).value),
password: String(row.getCell(2).value),
expected: String(row.getCell(3).value),
});
});
console.log(users);ผลลัพธ์:
[
{ username: 'standard_user', password: 'secret_sauce', expected: 'success' },
{ username: 'locked_out_user', password: 'secret_sauce', expected: 'error' },
{ username: 'problem_user', password: 'secret_sauce', expected: 'success' }
]eachRow({ includeEmpty: true }, cb) วนรวมแถวว่าง และ ws.eachCell(cb) / row.eachCell(cb) วนระดับเซลล์
เขียนผลกลับ ⭐
ws!.getCell('D2').value = 'PASS';
ws!.addRow(['new_user', 'pw', 'success']);
await wb.xlsx.writeFile('test-results/users-result.xlsx');ระวังค่าที่เซลล์คืนมา ⭐⭐
cell.value ไม่ได้เป็น string เสมอ อาจเป็น number, Date, object (สูตร, hyperlink, rich text) ควรแปลงก่อนใช้
ws!.getCell('A2').value; // 'abc' string
ws!.getCell('B2').value; // 123 number
ws!.getCell('C2').value; // 2026-10-29T00:00:00.000Z Date
ws!.getCell('D2').value; // { formula: 'A1+B1', result: 5 } สูตร
ws!.getCell('E2').value; // { text: 'link', hyperlink: 'https://...' }
ws!.getCell('F2').value; // null (เซลล์ว่าง)
const text = ws!.getCell('D2').text; // ใช้ .text เมื่ออยากได้ string ที่ปลอดภัย20.3 CSV: csv-parse
import fs from 'fs';
import { parse } from 'csv-parse/sync';
const records = parse(fs.readFileSync('tests/data/users.csv'), {
columns: true, // ใช้แถวแรกเป็นชื่อ key
skip_empty_lines: true,
trim: true,
bom: true, // ตัด BOM ที่ Excel ใส่ตอน Save as CSV UTF-8
});
console.log(records[0]); // { username: 'standard_user', password: 'secret_sauce', expected: 'success' }ตัวเลือกอื่น: delimiter: ';' (CSV ที่ใช้ semicolon), from_line: 2, cast: true (แปลงเลขอัตโนมัติ)
20.4 ฟังก์ชัน Node.js ที่ใช้คู่กัน
| ฟังก์ชัน | ใช้ทำอะไร | ตัวอย่าง (ผลลัพธ์) |
|---|---|---|
path.join(...) ⭐⭐⭐ | ต่อ path ข้าม OS | path.join('tests', 'data', 'a.xlsx') ('tests/data/a.xlsx') |
path.resolve(...) ⭐⭐ | path เต็ม | path.resolve('data/a.xlsx') ('C:/proj/data/a.xlsx') |
process.cwd() ⭐⭐ | โฟลเดอร์ที่รันคำสั่ง | ('C:/Github/test-playwright') |
__dirname ⭐⭐ | โฟลเดอร์ของไฟล์ (CommonJS เท่านั้น) | path.join(__dirname, 'data') |
fs.existsSync(p) ⭐⭐ | ไฟล์มีไหม | (true) |
fs.readFileSync(p) ⭐⭐ | อ่านไฟล์เป็น Buffer | fs.readFileSync(p, 'utf-8') ได้ string |
fs.writeFileSync(p, data) ⭐ | เขียนไฟล์ | fs.writeFileSync('out.json', json) |
fs.mkdirSync(p, { recursive: true }) ⭐ | สร้างโฟลเดอร์ | mkdirSync('test-results', { recursive: true }) |
fs.readdirSync(p) ⭐ | รายชื่อไฟล์ | (['users.xlsx', 'items.xlsx']) |
ถ้าโปรเจกต์ใช้ ES Module ("type": "module" ใน package.json) จะไม่มี __dirname ให้ใช้ process.cwd() หรือ import.meta.dirname (Node 20.11 ขึ้นไป) แทน
20.5 ใช้ใน Playwright: วนรันหลายชุดข้อมูลจาก Excel
อ่านไฟล์ที่ระดับบนสุดของไฟล์เทสต์ (นอก test()) เพื่อให้ Playwright เห็นรายการเทสต์ตอนเริ่มรัน แล้วสร้างเทสต์ทีละแถว
import { test, expect } from '@playwright/test';
import * as XLSX from 'xlsx';
import path from 'path';
type LoginRow = { username: string; password: string; expected: 'success' | 'error' };
const wb = XLSX.readFile(path.join(process.cwd(), 'tests', 'data', 'users.xlsx'));
const rows = XLSX.utils.sheet_to_json<LoginRow>(wb.Sheets['Login'], { defval: '' });
for (const row of rows) {
test(`login: ${row.username} -> ${row.expected}`, async ({ page }) => {
await page.goto('https://www.saucedemo.com');
await page.getByTestId('username').fill(row.username);
await page.getByTestId('password').fill(row.password);
await page.getByTestId('login-button').click();
if (row.expected === 'success') {
await expect(page).toHaveURL(/inventory/);
} else {
await expect(page.getByTestId('error')).toBeVisible();
}
});
}ผลลัพธ์การรัน:
Running 3 tests using 3 workers
✓ 1 [chromium] › login.spec.ts:15:7 › login: standard_user -> success (1.9s)
✓ 2 [chromium] › login.spec.ts:15:7 › login: locked_out_user -> error (1.4s)
✓ 3 [chromium] › login.spec.ts:15:7 › login: problem_user -> success (2.0s)
3 passed (4.1s)ตั้งชื่อเทสต์ให้มีค่าจากแถว (เช่น row.username) ไม่เช่นนั้นชื่อซ้ำกันและ Playwright จะ error ว่า duplicate test title
20.6 ใช้กับ ExcelJS (async)
ExcelJS อ่านแบบ async จึงเรียกที่ระดับบนสุดไม่สะดวก ให้โหลดข้อมูลใน test.beforeAll หรือใช้ไฟล์ตั้งค่า globalSetup แล้วอ่านข้อมูลต่อเทสต์ในรูปแบบนี้
import { test, expect } from '@playwright/test';
import ExcelJS from 'exceljs';
let users: { username: string; password: string }[] = [];
test.beforeAll(async () => {
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile('tests/data/users.xlsx');
const ws = wb.getWorksheet('Login')!;
ws.eachRow((row, n) => {
if (n === 1) return;
users.push({ username: String(row.getCell(1).value), password: String(row.getCell(2).value) });
});
});
test('ใช้แถวแรก', async ({ page }) => {
await page.goto('https://www.saucedemo.com');
await page.getByTestId('username').fill(users[0].username);
await page.getByTestId('password').fill(users[0].password);
await page.getByTestId('login-button').click();
await expect(page).toHaveURL(/inventory/);
});ข้อจำกัด: ถ้าต้องการสร้างหลายเทสต์จากหลายแถวแบบหัวข้อ 20.5 ให้ใช้ SheetJS (sync) หรือแปลง Excel เป็น JSON ไว้ล่วงหน้าแล้วอ่านด้วย JSON.parse(fs.readFileSync(...))
20.7 เขียนผลเทสต์กลับ Excel
import * as XLSX from 'xlsx';
const results: { test: string; status: string; time: string }[] = [];
test.afterEach(async ({}, testInfo) => {
results.push({ test: testInfo.title, status: testInfo.status ?? 'unknown', time: `${testInfo.duration} ms` });
});
test.afterAll(() => {
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(results), 'Results');
XLSX.writeFile(wb, 'test-results/summary.xlsx');
});ข้อควรระวัง: ถ้ารันหลาย worker พร้อมกัน ตัวแปร results จะแยกกันต่อ worker ให้ตั้ง workers: 1 หรือใช้ custom reporter รวมผล
20.8 ข้อผิดพลาดที่พบบ่อยตอนอ่าน Excel
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
ENOENT: no such file or directory | path ผิด (ขึ้นกับโฟลเดอร์ที่รันคำสั่ง) | ใช้ path.join(process.cwd(), ...) และเช็ก fs.existsSync |
| ค่าเซลล์ว่างทำให้ key หาย | ไม่ตั้ง defval | sheet_to_json(ws, { defval: '' }) |
ตัวเลขนำหน้า 0 หาย (0812345678 กลายเป็น 812345678) | เซลล์เป็นชนิดตัวเลข | ตั้งคอลัมน์เป็น Text ใน Excel หรืออ่านด้วย raw: false และ String(...).padStart(10, '0') |
| วันที่ได้เป็นเลข 46324 | ไม่ได้ใส่ cellDates | XLSX.readFile(f, { cellDates: true }) |
ExcelJS คืนค่าเป็น { formula, result } | เซลล์มีสูตร | ใช้ cell.result หรือ cell.text |
| ExcelJS ได้ index เลื่อน 1 | row.values นับจาก 1 | ใช้ getCell(n) แทน หรือ .values.slice(1) |
| Excel เปิดค้างอยู่แล้วอ่านไม่ได้ (Windows) | ไฟล์ถูกล็อก | ปิดไฟล์ก่อนรัน หรือคัดลอกไฟล์ก่อนอ่าน |
| CSV ภาษาไทยเพี้ยน | encoding ไม่ใช่ UTF-8 | Save as CSV UTF-8 และตั้ง bom: true ใน csv-parse |
duplicate test title | ชื่อเทสต์จากแต่ละแถวซ้ำกัน | ใส่ค่าที่ไม่ซ้ำ หรือเลขแถวในชื่อ: `row ${i + 1}: ${row.username}` |
| ลืมแปลงชนิดค่า | เลขใน Excel เป็น number แต่ fill() ต้องการ string | fill(String(row.qty)) |
20.9 ทางเลือกที่ง่ายกว่า: แปลง Excel เป็น JSON ล่วงหน้า
ถ้าไม่อยากพึ่งไลบรารีตอนรันเทสต์ ให้แปลงครั้งเดียวด้วยสคริปต์ แล้วให้เทสต์อ่าน JSON
// scripts/excel-to-json.ts รัน: npx ts-node scripts/excel-to-json.ts
import * as XLSX from 'xlsx';
import fs from 'fs';
const wb = XLSX.readFile('tests/data/users.xlsx');
const data = XLSX.utils.sheet_to_json(wb.Sheets['Login'], { defval: '' });
fs.writeFileSync('tests/data/users.json', JSON.stringify(data, null, 2));// ในเทสต์
import users from './data/users.json'; // ต้องเปิด resolveJsonModule ใน tsconfig.jsonข้อดีคือเทสต์เร็วขึ้นและรองรับ import โดยตรง ข้อเสียคือต้องรันสคริปต์แปลงใหม่ทุกครั้งที่แก้ Excel
🔗 ลิงก์เรียนรู้ Playwright และ TypeScript สำหรับมือใหม่ (25 ลิงก์)
แยกภาษาไทย/อังกฤษ เรื่องที่ค้นหาภาษาไทยพบมีน้อย (Playwright 2 ลิงก์, TypeScript 3 ลิงก์) บางบทความเก่า 2–3 ปี ควรเทียบกับเอกสารทางการที่ playwright.dev ลิงก์ที่ระบุว่า “ยังไม่ได้เปิดตรวจ” คือ URL ที่ผมคาดจากโครงสร้างเว็บทางการ ยังไม่ได้เปิดดูโดยตรง