Automation testing

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

Phong Do Phong Do · 07/09/2025 · 18 phút đọc

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`);
});

Tìm Hiểu Thêm

Phong Do
Tác giả
Phong Do