Skip to content

Mengunci Intent Desain dengan Test, Bukan Dokumentasi

Adityo Guni Waluyo

Mengunci intent desain bukan dengan dokumentasi tebal, tapi dengan unit test yang memaksa class Tailwind tetap sesuai aturan token.

Ringkasan

Abis restyle komponen, desainnya rapi bentar terus balik berantakan lagi gara-gara ketimpa kode dari branch lain. Bikin dokumen setebel apapun ternyata nggak ngaruh, akhirnya gue kunci pola class-nya pake unit test 129 baris yang ngecek string HTML. Jadi kalau ada yang nggak sengaja balikin style lama kayak bg-white atau rounded-xl, test-nya langsung meledak dan ketahuan di CI.

Dua minggu setelah restyle komponen data, saya buka lagi halaman layanan. Tiga card mode PoCC yang kemarin saya rapikan, dan salah satunya udah balik ke bentuk lama: border tebal, background putih polos, sudut rounded-xl. Saya belum ngedit apa-apa. Yang saya lakuin cuma narik kode dari branch lain buat fitur sebelah.

Itu bukan kejadian pertama. Pola ini yang bikin saya kapok kerjain restyle visual: minggu pertama rapi, minggu kedua mulai measured, minggu ketiga ada yang nyasarkelas. Screenshot di design doc menua. Reviewer lupa. Dan tiap audit kuartalan yang nemuin, bukan nangkep.

Tebakan saya waktu itu: masalahnya komunikasi. Dokumentasinya kurang tebal, screenshotnya kurang detail. Jadi saya bikin design doc makin lengkap, lengkap dengan spesifikasi class per komponen. Hasilnya nol. Dokumen yang makin tebal nggak pernah berhasil mengalahkan ingatan yang makin tipis.

Di restyle terakhir, saya nyoba jalan yang beda. Setelah intent desain di-fix di dokumen, saya tulis file unit test 129 baris yang tugasnya cuma satu: mastiin pola class-nya nggak bisa geser diam-diam.

Test yang ngecek string, bukan browser

File data-component-restyle.test.ts nggak butuh browser sama sekali. Enam komponen dirender pake renderToStaticMarkup, fungsi yang merender pohon React jadi string HTML non-interaktif [1]. Outputnya emang nggak bisa di-hydrate, tapi buat ngecek string class, kelewat cukup.

Komponennya pake next/image, dan test nggak mau tau soal itu. Satu baris ngegantikan modulnya:

vi.mock("next/image", () => ({ default: () => null }));

import PoccSection from "./pocc";

Urutannya bukan kebetulan. vi.mock di-hoist di atas semua import [2], jadi walaupun barisnya ditulis setelah import komponen, mock-nya selalu kepasan duluan sebelum modul komponen ke-load.

Asersinya sendiri sederhana. Helper count() ngehitung berapa kali string class muncul di output, jadi saya bisa mastiin tiga card mode PoCC pake tint Pattern A rounded-2xl border border-primary/15 bg-card/40 yang persis sama, dan empat card workforce pake ikon lucide size-8 text-accent yang sama besar.

it("pocc: mode cards use Pattern A tint with lead icons", () => {
  const html = render(PoccSection, { content: pocc });
  expect(count(html, "rounded-2xl border border-primary/15 bg-card/40 p-lg")).toBe(3);
  expect(count(html, "size-8 text-accent")).toBe(3);
  expect(html).not.toContain("rounded-xl");
});

Baris paling penting justru yang negatif. not.toContain("bg-white") dan not.toContain("rounded-xl") itu penjaga buat class lama. Kalau ada yang narik pola lama lewat branch lain, test-nya yang meledak duluan, bukan audit tiga bulan kemudian.

Aturan tanpa penegak cuma harapan

Aturan house di project ini jelas: token-only, nol hex baru, Pattern A buat card di surface tint. Aturan kayak gini mentok-mentoknya cuma jadi doa kalau penegakannya diserahkan ke disiplin tim. Test biasanya ngepin behavior; yang bikin pendekatan ini beda, test-nya ngepin contract desain. Justru karena namanya token yang jadi API publik sistem desain di Tailwind [4], string class itu layak dikunci: geser dikit, artinya berubah.

Prinsip Testing Library bilang makin mirip test dengan cara software dipake, makin besar kepercayaannya [3]. Asersi string class jelas bukan semangat itu. Ini trade-off yang saya ambil sadar: failure mode yang dijaga bukan behavior yang rusak, tapi pola yang geser. Untuk behavior, ada test lain. Untuk desain, ini penjaganya.

Urutannya juga penting. Intent-nya di-fix duluan di design doc, test ditulis setelahnya. Test-after tetap valid karena yang dikunci bukan tebakan implementasi, tapi keputusan yang udah diputuskan.

Hasil di CI: suite 20/20 PASS barengan build PASS. Grep diff: nol hex baru, nol font-family baru. Angka yang sama kayak di TESTLOG, cuma kali ini yang mastiin itu test, bukan niat baik.

Sekarang kalo ada aturan desain yang sifatnya mutlak, saya nggak tanya "udah didokumentasin belum" lagi. Yang saya tanya: "test mana yang meledak kalo aturan ini dilanggar?" Kalo jawabannya nggak ada, aturan itu belum selesai. Dan keputusan besok pagi: satu asersi buat rule validator token, satu lagi buat cegah text-accent-text balik ke heading. Biar mesin yang jaga, saya tinggal tidur.

Sumber:

  1. React docs: renderToStaticMarkup
  2. Vitest docs: vi.mock
  3. Testing Library: Guiding Principles
  4. Tailwind CSS: Theme variables

Artikel terkait