Praktik dark pattern seperti sneak into basket (penambahan add-on otomatis), drip pricing (biaya tersembunyi yang muncul di akhir alur), dan confirmshaming (manipulasi visual tombol pembatalan) sering kali lolos ke tahap produksi akibat regresi antarmuka pengguna atau eksperimen A/B testing yang tidak terkontrol. Pengujian visual manual tidak memadai untuk mendeteksi perubahan state form dan diskrepansi kalkulasi biaya dinamis secara konsisten.
Artikel ini menjabarkan strategi implementasi automated End-to-End (E2E) testing berbasis Playwright untuk memvalidasi kepatuhan alur checkout terhadap prinsip clear consent dan integritas transaksi. Fokus pengujian mencakup penegakan status default unselected, audit matematis rincian pembayaran, serta pengujian ketegasan tombol aksi menggunakan selector berbasis aksesibilitas.
1. Assertion Status Default Opt-out pada Add-on Opsional
Regulasi perlindungan konsumen dan regulasi privasi modern mewajibkan layanan digital menerapkan explicit opt-in untuk setiap item bernilai tambah yang dikenakan biaya, seperti asuransi perjalanan, garansi tambahan, atau layanan prioritas.
Untuk menguji aturan ini, runner pengujian harus memastikan bahwa seluruh input add-on selalu berstatus unchecked saat komponen pertama kali dirender dan setelah interaksi pengguna dengan elemen lain di sekitarnya.
import { test, expect } from '@playwright/test';
test.describe('Checkout - Addon Opt-in Verification', () => {
test('Add-on asuransi dan prioritas harus berstatus unselected secara default', async ({ page }) => {
await page.goto('/checkout/addons');
const insuranceCheckbox = page.getByRole('checkbox', { name: /asuransi perjalanan/i });
const priorityBagCheckbox = page.getByRole('checkbox', { name: /bagasi prioritas/i });
// Verifikasi elemen terlihat dan tidak dipilih secara otomatis
await expect(insuranceCheckbox).toBeVisible();
await expect(insuranceCheckbox).not.toBeChecked();
await expect(priorityBagCheckbox).toBeVisible();
await expect(priorityBagCheckbox).not.toBeChecked();
// Verifikasi state DOM native dan ARIA attribute
const ariaChecked = await insuranceCheckbox.getAttribute('aria-checked');
expect(ariaChecked === 'false' || ariaChecked === null).toBeTruthy();
});
});Penggunaan expect(locator).not.toBeChecked() memvalidasi native checkbox maupun kustom checkbox berbasis ARIA standar. Ini mencegah manipulasi CSS di mana elemen native disembunyikan dan pseudo-elemen kustom menampilkan centang tanpa persetujuan eksplisit.
2. Memverifikasi Konsistensi Rincian Harga Antar-Langkah
Drip pricing terjadi ketika harga yang ditampilkan di ringkasan awal berubah di langkah berikutnya tanpa adanya penambahan item oleh pengguna. Testing harus melakukan audit kalkulasi harga secara deterministik di setiap transisi halaman atau langkah wizard.
Fungsi Utilitas Normalisasi Mata Uang
Data harga yang diambil dari DOM sering kali mengandung pemisah ribuan dan simbol mata uang. Ekstrak string tersebut ke tipe numerik sebelum melakukan komparasi numerik.
function parseRupiah(priceText: string): number {
const sanitized = priceText.replace(/[^0-9]/g, '');
return parseInt(sanitized, 10);
}Audit State Finansial
Pastikan nilai total pada langkah akhir setara dengan penjumlahan nilai awal produk utama ditambah pajak dan biaya pengiriman resmi, tanpa adanya injeksi biaya penanganan terselubung.
test('Memverifikasi tidak ada biaya terselubung saat beralih ke pembayaran', async ({ page }) => {
await page.goto('/checkout/review');
// Ambil rincian biaya awal
const basePriceText = await page.getByTestId('summary-base-price').innerText();
const taxText = await page.getByTestId('summary-tax').innerText();
const initialTotalText = await page.getByTestId('summary-grand-total').innerText();
const basePrice = parseRupiah(basePriceText);
const tax = parseRupiah(taxText);
const initialTotal = parseRupiah(initialTotalText);
// Validasi perhitungan awal
expect(initialTotal).toBe(basePrice + tax);
// Navigasi ke langkah pembayaran
await page.getByRole('button', { name: /lanjut ke pembayaran/i }).click();
await page.waitForURL('**/checkout/payment');
// Ambil rincian biaya di halaman pembayaran
const finalTotalText = await page.getByTestId('payment-grand-total').innerText();
const finalTotal = parseRupiah(finalTotalText);
// Assertion ketat: total harga tidak boleh bertambah tanpa aksi pengguna
expect(finalTotal).toBe(initialTotal);
// Memastikan tidak ada line-item biaya siluman
const hiddenFeeItems = page.locator('[data-testid="line-item-hidden-fee"]');
await expect(hiddenFeeItems).toHaveCount(0);
});3. Membedakan Tombol Aksi Primer vs Upsell Modal
Teknik manipulatif sering menggunakan modal pop-up yang menyamarkan tombol penolakan (confirmshaming). Contoh: tombol "Tambahkan Proteksi" dibuat kontras tinggi (primary), sedangkan tombol "Saya Siap Menanggung Kerugian" dibuat transparan, tersembunyi, atau menyerupai teks non-interaktif.
Playwright dapat membedakan hierarki aksi ini secara fungsional dan semantik melalui strategi berikut:
test('Penolakan upsell modal harus jelas, interaktif, dan mudah diakses', async ({ page }) => {
await page.goto('/checkout/step-2');
// Trigger transisi yang berpotensi memunculkan upsell dialog
await page.getByRole('button', { name: /selanjutnya/i }).click();
const upsellDialog = page.getByRole('dialog');
if (await upsellDialog.isVisible()) {
// Tombol penolakan harus terdefinisi sebagai tombol yang valid dan terlihat
const declineButton = upsellDialog.getByRole('button', { name: /lewati|tidak, terima kasih/i });
await expect(declineButton).toBeVisible();
await expect(declineButton).toBeEnabled();
// Verifikasi tombol tidak disembunyikan menggunakan CSS trick
const opacity = await declineButton.evaluate((el) => window.getComputedStyle(el).opacity);
const pointerEvents = await declineButton.evaluate((el) => window.getComputedStyle(el).pointerEvents);
expect(Number(opacity)).toBeGreaterThan(0.5);
expect(pointerEvents).not.toBe('none');
// Klik tombol lewati dan pastikan dialog tertutup
await declineButton.click();
await expect(upsellDialog).toBeHidden();
}
// Verifikasi pengguna tetap berada pada jalur checkout yang benar
await expect(page).toHaveURL(/.*/checkout/final/);
});4. Pengujian Berbasis Aksesibilitas (Role & ARIA)
Menghindari manipulasi visual mengharuskan pengujian tidak bergantung semata pada CSS class dinamis (seperti .btn-secondary). Menguji struktur Accessibility Tree memastikan elemen pembatalan dan opt-out tetap dikenali oleh pembaca layar dan navigasi keyboard.
Peringatan: Menghindari dark pattern secara visual tidak cukup jika elemen opt-out diberi atributaria-hidden="true"atautabindex="-1"tanpa pemetaan keyboard yang setara. Praktik ini mendiskriminasi pengguna alat bantu aksesibilitas.
Checklist Verifikasi Aksesibilitas Form
- Accessible Name: Gunakan
getByRole('button', { name: '...' })untuk memvalidasi teks label terbaca jelas oleh mesin bantu. - Visual Obfuscation Check: Pastikan kontras teks penolakan tidak sengaja dibuat mendekati warna latar belakang melalui computed styles.
- Keyboard Operability: Validasi navigasi alur checkout sepenuhnya dapat diselesaikan menggunakan tombol
TabdanSpace/Enter.
test('Interaksi checkout harus dapat dioperasikan via keyboard tanpa jebakan fokus', async ({ page }) => {
await page.goto('/checkout/addons');
// Fokuskan pada add-on checkbox menggunakan keyboard tab
await page.keyboard.press('Tab');
const focusedElement = page.locator(':focus');
// Pastikan elemen yang terfokus memiliki peran interaktif
const role = await focusedElement.getAttribute('role');
const tagName = await focusedElement.evaluate((el) => el.tagName.toLowerCase());
expect(['checkbox', 'button', 'input'].includes(role || tagName)).toBeTruthy();
});5. Mitigasi Flakiness dan Best Practices
Pengujian alur checkout sering kali rapuh (flaky) terhadap latensi jaringan dan kalkulasi asinkron. Terapkan prinsip engineering berikut:
- Network Interception: Gunakan
page.route()untuk memvalidasi response payload dari endpoint pricing service secara deterministik tanpa bergantung pada fluktuasi server staging. - Web-first Assertions: Selalu gunakan assertion berbasis polling bawaan Playwright (misal:
await expect().toBeVisible()) dibandingkanpage.waitForTimeout()atau pengecekan boolean manualtoBe(true). - Isolated Test State: Bersihkan cookies, local storage, dan session storage di setiap awal worker untuk mencegah kebocoran status keranjang belanja dari pengujian sebelumnya.
Penerapan E2E testing yang terstruktur terhadap pola interaksi form dan integritas tagihan secara efektif mengeliminasi risiko masuknya regresi dark pattern ke siklus rilis produksi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!