Automation testing
[Vọc Playwright] Xử lý Field Dạng Số trong Playwright TypeScript

Khi thực hiện automation testing cho các form nhập liệu, việc xử lý các field dạng số là một trong những thách thức phổ biến. Các field này không chỉ đơn thuần là nhập số mà còn có nhiều biến thể như số nguyên, số thập phân, số âm, định dạng tiền tệ, hay các giới hạn về giá trị. Bài viết này sẽ hướng dẫn chi tiết cách xử lý các tình huống khác nhau khi test field dạng số với Playwright TypeScript.
Các Loại Field Dạng Số Thường Gặp
Trong thực tế, chúng ta thường gặp các loại input số sau:
Input type=”number”
Đây là loại phổ biến nhất trong HTML5, cho phép người dùng nhập số trực tiếp hoặc sử dụng các nút tăng giảm.
<!-- HTML Example -->
<form id="basic-form">
<label for="age">Age:</label>
<input
type="number"
id="age"
name="age"
min="18"
max="100"
required
/>
<label for="quantity">Quantity:</label>
<input
type="number"
id="quantity"
name="quantity"
min="1"
max="999"
step="1"
value="1"
/>
</form>
Input type=”text” với validation số
- Nhiều ứng dụng vẫn sử dụng input text thông thường nhưng áp dụng validation để chỉ chấp nhận số.
- Các field như số điện thoại, mã zip, số thẻ tín dụng thường có format riêng với dấu phân cách.
- Field nhập số tiền thường có định dạng với dấu phẩy ngăn cách hàng nghìn và dấu chấm cho phần thập phân.
<!-- HTML Example - Text input với regex pattern -->
<div class="custom-number-inputs">
<!-- Số điện thoại Việt Nam -->
<label for="phone">Phone Number (VN):</label>
<input
type="text"
id="phone"
name="phone"
pattern="^(0|\+84)[0-9]{9}$"
placeholder="0901234567"
maxlength="10"
/>
<!-- Mã ZIP -->
<label for="zipcode">ZIP Code:</label>
<input
type="text"
id="zipcode"
name="zipcode"
pattern="[0-9]{5}"
placeholder="70000"
maxlength="5"
/>
<!-- Số thẻ tín dụng với format -->
<label for="credit-card">Credit Card:</label>
<input
type="text"
id="credit-card"
name="credit-card"
pattern="[0-9]{4}\s?[0-9]{4}\s?[0-9]{4}\s?[0-9]{4}"
placeholder="1234 5678 9012 3456"
maxlength="19"
/>
</div>
Các Kỹ Thuật Xử Lý Cơ Bản
Nhập giá trị số đơn giản
import { test, expect, Page } from '@playwright/test';
test('Nhập số vào field cơ bản', async ({ page }) => {
await page.goto('https://example.com/form');
// Cách 1: Sử dụng fill() - xóa giá trị cũ và nhập mới
await page.locator('#quantity').fill('100');
// Cách 2: Sử dụng type() - nhập từng ký tự
await page.locator('#price').type('1500.50');
// Cách 3: Clear trước khi nhập
const amountField = page.locator('#amount');
await amountField.clear();
await amountField.fill('2500');
});
Xử lý field có giá trị mặc định
test('Xử lý field có giá trị sẵn', async ({ page }) => {
await page.goto('https://example.com/form');
const quantityField = page.locator('#quantity');
// Lấy giá trị hiện tại
const currentValue = await quantityField.inputValue();
console.log('Giá trị hiện tại:', currentValue);
// Clear bằng cách select all và ghi đè
await quantityField.click();
await page.keyboard.press('Control+A'); // Hoặc 'Meta+A' cho Mac
await quantityField.type('999');
// Verify giá trị mới
await expect(quantityField).toHaveValue('999');
});
Xử lý field có format tiền tệ
class CurrencyHelper {
// Chuyển đổi số thành format tiền tệ
static formatCurrency(value: number): string {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND'
}).format(value);
}
// Loại bỏ format để lấy số thuần
static parseCurrency(formattedValue: string): number {
return parseFloat(
formattedValue.replace(/[^\d.-]/g, '')
);
}
}
test('Nhập và verify field tiền tệ', async ({ page }) => {
await page.goto('https://example.com/payment');
const amountField = page.locator('#payment-amount');
const rawValue = 1500000;
// Nhập giá trị số thuần
await amountField.fill(rawValue.toString());
// Chờ field tự format
await page.waitForTimeout(500);
// Lấy giá trị đã format từ UI
const displayedValue = await amountField.inputValue();
// Parse và so sánh
const parsedValue = CurrencyHelper.parseCurrency(displayedValue);
expect(parsedValue).toBe(rawValue);
});
Validation Testing cho Field Số
Test giới hạn min/max
test.describe('Validation giới hạn số', () => {
test('Test giá trị ngoài phạm vi cho phép', async ({ page }) => {
await page.goto('https://example.com/form');
const ageField = page.locator('#age');
const errorMessage = page.locator('.error-message');
// Test giá trị nhỏ hơn min (min=18)
await ageField.fill('15');
await page.locator('#submit').click();
await expect(errorMessage).toContainText('Age must be at least 18');
// Test giá trị lớn hơn max (max=100)
await ageField.fill('150');
await page.locator('#submit').click();
await expect(errorMessage).toContainText('Age must not exceed 100');
// Test giá trị hợp lệ
await ageField.fill('25');
await page.locator('#submit').click();
await expect(errorMessage).not.toBeVisible();
});
});
Test input không phải số
test('Test nhập ký tự không phải số', async ({ page }) => {
await page.goto('https://example.com/form');
const numberField = page.locator('input[type="number"]');
// Thử nhập chữ cái
await numberField.fill('abc');
const value1 = await numberField.inputValue();
expect(value1).toBe(''); // Field number tự động reject
// Thử nhập ký tự đặc biệt
await numberField.fill('!@#');
const value2 = await numberField.inputValue();
expect(value2).toBe('');
// Thử nhập hỗn hợp
await numberField.fill('12abc34');
const value3 = await numberField.inputValue();
// Một số browser có thể chấp nhận '12' hoặc reject hoàn toàn
expect(['', '12']).toContain(value3);
});
Test số thập phân
test('Test xử lý số thập phân', async ({ page }) => {
await page.goto('https://example.com/calculator');
const priceField = page.locator('#price');
// Test số thập phân chuẩn
await priceField.fill('99.99');
await expect(priceField).toHaveValue('99.99');
// Test nhiều dấu chấm
await priceField.fill('99.99.99');
await page.locator('#calculate').click();
const errorMsg = page.locator('.validation-error');
await expect(errorMsg).toContainText('Invalid number format');
// Test độ chính xác (precision)
await priceField.fill('99.999'); // Giả sử chỉ cho phép 2 chữ số thập phân
await page.locator('#calculate').click();
const roundedValue = await priceField.inputValue();
expect(parseFloat(roundedValue)).toBe(100.00);
});
Xử Lý Các Tình Huống Đặc Biệt
Field có spinner buttons
test('Test tăng giảm bằng spinner buttons', async ({ page }) => {
await page.goto('https://example.com/shopping-cart');
const quantityField = page.locator('#item-quantity');
const increaseBtn = page.locator('#item-quantity ~ .spinner-up');
const decreaseBtn = page.locator('#item-quantity ~ .spinner-down');
// Set giá trị ban đầu
await quantityField.fill('5');
// Test nút tăng
await increaseBtn.click();
await expect(quantityField).toHaveValue('6');
// Test nút giảm
await decreaseBtn.click();
await decreaseBtn.click();
await expect(quantityField).toHaveValue('4');
// Test giới hạn min (thường là 1 cho quantity)
for (let i = 0; i < 10; i++) {
await decreaseBtn.click();
}
await expect(quantityField).toHaveValue('1');
});
Field với mask input
test('Test field có input mask', async ({ page }) => {
await page.goto('https://example.com/contact');
const phoneField = page.locator('#phone');
// Nhập số điện thoại không có format
await phoneField.fill('0901234567');
// Chờ mask tự động format
await page.waitForTimeout(300);
// Verify format đã được áp dụng
const formattedPhone = await phoneField.inputValue();
expect(formattedPhone).toMatch(/^\(\d{3}\) \d{3}-\d{4}$/);
// Ví dụ: (090) 123-4567
});
Field tính toán tự động
test('Test field tự động tính toán', async ({ page }) => {
await page.goto('https://example.com/invoice');
const quantityField = page.locator('#quantity');
const priceField = page.locator('#unit-price');
const totalField = page.locator('#total');
// Nhập quantity và price
await quantityField.fill('10');
await priceField.fill('150.50');
// Trigger tính toán (có thể là blur event)
await priceField.blur();
// Chờ và verify kết quả
await expect(totalField).toHaveValue('1505.00', { timeout: 2000 });
// Test thay đổi giá trị
await quantityField.fill('5');
await quantityField.blur();
await expect(totalField).toHaveValue('752.50', { timeout: 2000 });
});
Best Practices khi Test Field Số
Tạo helper functions tái sử dụng
class NumberFieldHelper {
private page: Page;
constructor(page: Page) {
this.page = page;
}
async enterNumber(selector: string, value: number): Promise<void> {
const field = this.page.locator(selector);
await field.clear();
await field.fill(value.toString());
}
async getNumericValue(selector: string): Promise<number> {
const field = this.page.locator(selector);
const textValue = await field.inputValue();
return parseFloat(textValue.replace(/[^\d.-]/g, ''));
}
async verifyRange(
selector: string,
min: number,
max: number
): Promise<boolean> {
const value = await this.getNumericValue(selector);
return value >= min && value <= max;
}
}
test('Sử dụng helper class', async ({ page }) => {
const helper = new NumberFieldHelper(page);
await page.goto('https://example.com/form');
await helper.enterNumber('#age', 25);
const isValidAge = await helper.verifyRange('#age', 18, 100);
expect(isValidAge).toBeTruthy();
});
Data-driven testing cho các scenarios số
interface NumberTestCase {
input: string;
expected: string;
shouldPass: boolean;
description: string;
}
const testCases: NumberTestCase[] = [
{ input: '100', expected: '100', shouldPass: true, description: 'Số nguyên dương' },
{ input: '-50', expected: '-50', shouldPass: true, description: 'Số nguyên âm' },
{ input: '99.99', expected: '99.99', shouldPass: true, description: 'Số thập phân' },
{ input: '0', expected: '0', shouldPass: true, description: 'Số không' },
{ input: 'abc', expected: '', shouldPass: false, description: 'Chữ cái' },
{ input: '1e10', expected: '10000000000', shouldPass: true, description: 'Ký hiệu khoa học' },
];
testCases.forEach(testCase => {
test(`Test input: ${testCase.description}`, async ({ page }) => {
await page.goto('https://example.com/form');
const numberField = page.locator('#number-input');
await numberField.fill(testCase.input);
await page.locator('#validate').click();
if (testCase.shouldPass) {
await expect(numberField).toHaveValue(testCase.expected);
await expect(page.locator('.error')).not.toBeVisible();
} else {
await expect(page.locator('.error')).toBeVisible();
}
});
});
Xử Lý Business Logic Phức Tạp
Validation phụ thuộc
test('Test validation phụ thuộc giữa các field', async ({ page }) => {
await page.goto('https://example.com/loan-calculator');
const incomeField = page.locator('#monthly-income');
const loanAmountField = page.locator('#loan-amount');
const errorMessage = page.locator('.validation-message');
// Nhập thu nhập
await incomeField.fill('5000000');
// Test loan amount vượt quá giới hạn cho phép (giả sử max = income * 10)
await loanAmountField.fill('60000000');
await page.locator('#calculate').click();
await expect(errorMessage).toContainText(
'Loan amount cannot exceed 10 times your monthly income'
);
// Test loan amount hợp lệ
await loanAmountField.fill('40000000');
await page.locator('#calculate').click();
await expect(errorMessage).not.toBeVisible();
});
Test performance với số lượng lớn
test('Test performance khi xử lý nhiều field số', async ({ page }) => {
await page.goto('https://example.com/bulk-entry');
const startTime = Date.now();
// Nhập data cho 100 field số
for (let i = 1; i <= 100; i++) {
const fieldSelector = `#number-field-${i}`;
await page.locator(fieldSelector).fill(String(Math.random() * 1000));
}
// Submit form
await page.locator('#submit-all').click();
// Đợi xử lý hoàn tất
await expect(page.locator('.success-message')).toBeVisible();
const endTime = Date.now();
const executionTime = endTime - startTime;
// Verify thời gian xử lý không quá 10 giây
expect(executionTime).toBeLessThan(10000);
console.log(`Execution time: ${executionTime}ms`);
});
